Moving to GTK!

master
Melroy van den Berg 2020-11-28 23:47:34 +01:00
parent 75a16a9e3f
commit 56fc03e18b
14 changed files with 411 additions and 212 deletions

9
cmake/doxygen.cmake Normal file
View File

@ -0,0 +1,9 @@
find_package(Doxygen REQUIRED)
set(DOXYGEN_CONFIG ${CMAKE_CURRENT_BINARY_DIR}/../doxygen.conf)
# note the option ALL which allows to build the docs together with the application
add_custom_target(Doxygen ALL
COMMAND ${DOXYGEN_EXECUTABLE} ${DOXYGEN_CONFIG}
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
COMMENT "Generating API documentation with Doxygen"
VERBATIM)

View File

@ -45,11 +45,11 @@ We can try using a [QGraphicsScene](https://doc.qt.io/qt-5/qgraphicsscene.html)
**Conclusion:** No, QGraphics will NOT be used, way too slow.
### GTK+
### GTK
#### GTK + Cairo
GTK+ is using the Cairo 2D graphics renderer using rasterization (CPU) bvy default. But Cairo also including backends for acceleration and for vector output formats. Can be used for both display and export to PDF/SVG.
GTK is using the Cairo 2D graphics renderer using rasterization (CPU) bvy default. But Cairo also including backends for acceleration and for vector output formats. Can be used for both display and export to PDF/SVG. GTK is also much more lightweight in comparison with Qt.
Problem is its only raster graphics using the CPU. Which isn't that bad, but I really miss hardware acceleration via OpenGL/Vulkan (cairo-gl will never be stable and is a dead-end). There is no vector generation. Cairo can however be used [together with SDL](https://www.cairographics.org/SDL/) they claim.

View File

@ -1,9 +1,7 @@
# TODO: Maybe move this CMake file to cmake folder and include it in the top-level Cmake file?
set (CMAKE_INCLUDE_CURRENT_DIR ON)
set (CMAKE_AUTOUIC ON)
set (CMAKE_AUTOMOC ON)
set (CMAKE_AUTORCC ON)
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
set(CMAKE_CXX_FLAGS "-Wall -Wextra")
set(CMAKE_CXX_FLAGS_DEBUG "-g")
@ -11,54 +9,41 @@ set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG")
set (CMAKE_CXX_STANDARD 20)
set (CMAKE_CXX_STANDARD_REQUIRED ON)
if(NOT CYGWIN)
set(CMAKE_CXX_EXTENSIONS OFF)
endif()
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
set(PROJECT_TARGET browser)
set(THREADS_PREFER_PTHREAD_FLAG ON)
find_package(Threads REQUIRED)
# QtCreator supports the following variables for Android, which are identical to qmake Android variables.
# Check http://doc.qt.io/qt-5/deployment-android.html for more information.
# They need to be set before the find_package(Qt5 ...) call.
find_library(libcmark-gfm libcmark-gfm.so REQUIRED)
find_library(libcmark-gfm-extensions libcmark-gfm-extensions.so REQUIRED)
#if(ANDROID)
# set(ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android")
# if (ANDROID_ABI STREQUAL "armeabi-v7a")
# set(ANDROID_EXTRA_LIBS
# ${CMAKE_CURRENT_SOURCE_DIR}/path/to/libcrypto.so
# ${CMAKE_CURRENT_SOURCE_DIR}/path/to/libssl.so)
# endif()
#endif()
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTKMM gtkmm-3.0)
pkg_check_modules(CAIRO REQUIRED cairo)
find_package (Qt5 COMPONENTS Widgets REQUIRED)
find_library (libcmark-gfm libcmark-gfm.so REQUIRED)
find_library (libcmark-gfm-extensions libcmark-gfm-extensions.so REQUIRED)
add_definitions(${GTKMM_CFLAGS_OTHER})
if("${CMAKE_BUILD_TYPE}" STREQUAL "Release")
# Include CPack packaging settings
#include(packaging)
endif()
set(SOURCES
main.cc
md-parser.cc
md-parser.h
renderer-interface.h
qt-renderer.cc
qt-renderer.h
imgui-renderer.cc
imgui-renderer.h
mainwindow.cc
mainwindow.h
scene.h
scene.cc
md-parser.cc
md-parser.h
render-area.cc
render-area.h
)
# TODO: Why is the executable 'browser' detetected as shared lib? I Just want to have an executable
# Maybe because I build shared libs in other CMakeLists, strange..?
if(ANDROID)
add_library (${PROJECT_TARGET} SHARED
${SOURCES}
)
else()
add_executable (${PROJECT_TARGET}
${SOURCES}
)
endif()
add_executable(${PROJECT_TARGET} ${SOURCES})
# Add workaround for std filesystem in older GCC versions
if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
@ -72,18 +57,25 @@ if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
endif()
endif()
# Add to include list the cmark binary directory for config.h, .._version.h & .._export.h
get_property(cmark_binary_dir GLOBAL PROPERTY COMMONMARKER_BINARY_DIR)
include_directories(${cmark_binary_dir})
# Get include list the cmark binary directory for config.h, .._version.h & .._export.h
# Get include list the cmark extensions binary directory for ..._export.h
get_property(CMAKE_BINARY_DIR GLOBAL PROPERTY COMMONMARKER_BINARY_DIR)
get_property(CMAKE_EXTENSIONS_BINARY_DIR GLOBAL PROPERTY COMMONMARKER_EXTENSIONS_BINARY_DIR)
# Add to include list the cmark extensions binary directory for ..._export.h
get_property(cmark_extensions_binary_dir GLOBAL PROPERTY COMMONMARKER_EXTENSIONS_BINARY_DIR)
include_directories(${cmark_extensions_binary_dir})
target_include_directories(${PROJECT_TARGET} PRIVATE
${PROJECT_SOURCE_DIR}/include
${CMAKE_BINARY_DIR}
${CMAKE_EXTENSIONS_BINARY_DIR}
${GTKMM_INCLUDE_DIRS}
${CAIRO_INCLUDE_DIRS}
lib/commonmarker/src
)
include_directories(lib/commonmarker/src)
target_link_directories(${PROJECT_TARGET} PRIVATE
${GTKMM_LIBRARY_DIRS}
${CAIRO_LIBRARY_DIRS}
)
# TODO: 1x target_link_libraries?
target_link_libraries (${PROJECT_TARGET} LINK_PUBLIC LibCommonMarker LibCommonMarkerExtensions ${CXX_FILESYSTEM_LIBRARIES})
target_link_libraries (${PROJECT_TARGET} PRIVATE Qt5::Widgets)
target_link_libraries(${PROJECT_TARGET} PRIVATE LibCommonMarker LibCommonMarkerExtensions Threads::Threads ${CXX_FILESYSTEM_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} ${GTKMM_LIBRARIES} ${CAIRO_LIBRARIES})
install(TARGETS ${PROJECT_TARGET} RUNTIME DESTINATION "bin" COMPONENT applications)

View File

View File

View File

@ -1,13 +1,13 @@
#include "mainwindow.h"
#include <QApplication>
#include <gtkmm/application.h>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
auto app = Gtk::Application::create(argc, argv, "org.gtkmm.example");
MainWindow window;
window.show();
return app.exec();
//Shows the window and returns when it is closed.
return app->run(window);
}

View File

@ -1,12 +1,8 @@
#include "mainwindow.h"
#include "scene.h"
#include "md-parser.h"
#include "qt-renderer.h"
#include <chrono>
#include <iostream>
#ifdef LEGACY_CXX
#include <experimental/filesystem>
namespace n_fs = ::std::experimental::filesystem;
@ -15,64 +11,31 @@ namespace n_fs = ::std::experimental::filesystem;
namespace n_fs = ::std::filesystem;
#endif
#include <QVBoxLayout>
#include <QMenuBar>
#include <QMainWindow>
#include <QWidget>
#include <QTextEdit>
#include <QSizePolicy>
#include <QGraphicsView>
#include <QRectF>
#include <QGraphicsTextItem>
MainWindow::MainWindow()
{
resize(600, 400);
setWindowTitle("Browser");
set_title("Browser");
set_default_size(800, 600);
QMenu fileMenu("File");
fileMenu.addAction("Open...");
fileMenu.addAction("New...");
fileMenu.addAction("Exit");
add(m_scrolledWindow);
menuBar()->addMenu(&fileMenu);
m_scrolledWindow.add(m_renderArea);
m_scrolledWindow.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
m_renderArea.show();
m_scrolledWindow.show();
QWidget *centralWidget = new QWidget;
setCentralWidget(centralWidget);
QVBoxLayout *layout = new QVBoxLayout(centralWidget);
layout->setContentsMargins(5, 5, 5, 5);
scene = new Scene(this);
view = new QGraphicsView(scene);
//view->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
view->setAlignment(Qt::Alignment::enum_type::AlignTop|Qt::Alignment::enum_type::AlignLeft);
layout->addWidget(view);
// We will not use TextEdit, it does only support HTML (and markdown, but we don't want to use the built-in parser).
// Instead, we can try QPainter in Qt or use a 2D engine (using ttf, glyphs atlas, render bitmap text).
textEdit = new QTextEdit();
textEdit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
layout->addWidget(textEdit);
// Setup parser & renderer
// Setup parser
setupParser();
}
MainWindow::~MainWindow()
{
delete scene;
delete view;
delete textEdit;
delete parser;
delete renderer;
delete menuBar();
}
void MainWindow::setupParser()
{
parser = new Parser();
renderer = new QtRenderer();
renderer->setScene(scene);
std::string exePath = n_fs::current_path().string();
std::string htmlOutput = "";
@ -82,53 +45,8 @@ void MainWindow::setupParser()
cmark_node *root_node = parser->parseFile(filePath);
if (root_node != NULL) {
htmlOutput = parser->renderHTML(root_node);
typedef std::chrono::high_resolution_clock Time;
typedef std::chrono::milliseconds ms;
typedef std::chrono::duration<float> fsec;
auto t0 = Time::now();
// Render AST to scene
renderer->renderDocument(root_node);
auto t1 = Time::now();
fsec fs = t1 - t0;
ms d = std::chrono::duration_cast<ms>(fs);
std::cout << "My render: " << d.count() << " ms" << std::endl;
// Render AST to drawing area
m_renderArea.renderDocument(root_node);
cmark_node_free(root_node);
}
setOutputToTextEdit(QString::fromStdString(htmlOutput));
}
/**
* Can be used for resizing the scene
void MainWindow::resizeEvent(QResizeEvent *) {
QRectF bounds = scene->itemsBoundingRect();
bounds.setWidth(bounds.width()*0.9); // to tighten-up margins
bounds.setHeight(bounds.height()*0.9); // same as above
view->fitInView(bounds, Qt::KeepAspectRatio);
view->centerOn(0, 0);
}*/
/**
* Example of adding text (plaintext or html) to text edit
*/
void MainWindow::setOutputToTextEdit(const QString& text)
{
typedef std::chrono::high_resolution_clock Time;
typedef std::chrono::milliseconds ms;
typedef std::chrono::duration<float> fsec;
auto htmlStart = Time::now();
// Possible rendering is done in seperate thread? Returning this function faster.
textEdit->setHtml(text);
auto htmlEnd = Time::now();
fsec diff = htmlEnd - htmlStart;
ms htmlDuration = std::chrono::duration_cast<ms>(diff);
std::cout << "HTML: " << htmlDuration.count() << " ms" << std::endl;
}

View File

@ -1,38 +1,30 @@
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <gtkmm/window.h>
#include <gtkmm/scrolledwindow.h>
#include "render-area.h"
class Scene;
class Parser;
class QtRenderer;
QT_BEGIN_NAMESPACE
class QAction;
class QTextEdit;
class QGraphicsView;
QT_END_NAMESPACE
class MainWindow : public QMainWindow
class MainWindow : public Gtk::Window
{
Q_OBJECT
public:
MainWindow();
~MainWindow();
void setOutputToTextEdit(const QString& text);
virtual ~MainWindow();
//private slots:
// void newFile();
protected:
// Signal handlers:
// Our new improved on_button_clicked(). (see below)
void on_button_clicked(Glib::ustring data);
// Child widgets
Gtk::ScrolledWindow m_scrolledWindow;
RenderArea m_renderArea;
private:
Scene *scene;
QGraphicsView *view;
QTextEdit *textEdit;
Parser *parser;
QtRenderer *renderer;
void setupParser();
// void resizeEvent(QResizeEvent *);
void setupParser();
};
#endif

View File

@ -2,6 +2,9 @@
#include "scene.h"
#include "cmark-gfm.h"
#include <chrono>
#include <iostream>
#include <node.h>
#include <QFont>
#include <QGraphicsTextItem>
@ -215,6 +218,11 @@ void QtRenderer::renderNode(cmark_node *node, cmark_event_type ev_type)
QRectF const QtRenderer::drawText(const QString& text)
{
typedef std::chrono::high_resolution_clock Time;
typedef std::chrono::milliseconds ms;
typedef std::chrono::duration<float> fsec;
auto t0 = Time::now();
// We can still extend the QGraphicsSimpleTextItem class (or QAbstractGraphicsShapeItem) and override paint method.
// Or just use QPainter with a paint device (like QWidgets), to have maximal control.
QGraphicsSimpleTextItem *textItem = new QGraphicsSimpleTextItem(text);
@ -252,6 +260,11 @@ QRectF const QtRenderer::drawText(const QString& text)
font->setPixelSize(defaultFontSize);
}
auto t1 = Time::now();
fsec fs = t1 - t0;
ms d = std::chrono::duration_cast<ms>(fs);
std::cout << "Draw Text: " << d.count() << " ms . Content: " << text.toStdString().c_str() << std::endl;
return textItem->boundingRect();
}

281
src/render-area.cc Normal file
View File

@ -0,0 +1,281 @@
#include "render-area.h"
#include "node.h"
RenderArea::RenderArea()
: currentX(0),
currentY(0),
sceneMarginX(3),
sceneMarginY(3),
headingLevel(0),
listLevel(0),
wordSpacing(4), // spacing may depend on the font
heighestHigh(0),
paragraphHeightOffset(5),
headingHeightOffset(10),
listXOffset(15),
bulletPointDynamicOffset(0)
{
// Resize the drawing area to get scroll bars
set_size_request(800, 1000);
}
RenderArea::~RenderArea()
{
}
void RenderArea::renderDocument(cmark_node *root_node)
{
cmark_event_type ev_type;
cmark_iter *iter = cmark_iter_new(root_node);
while ((ev_type = cmark_iter_next(iter)) != CMARK_EVENT_DONE) {
cmark_node *cur = cmark_iter_get_node(iter);
renderNode(cur, ev_type);
}
// TODO: call the draw text/etc. methods below.
}
/**
* Calculates the locations, render and paint the content/objects
* to a QGraphicsScene
*/
void RenderArea::renderNode(cmark_node *node, cmark_event_type ev_type)
{
bool entering = (ev_type == CMARK_EVENT_ENTER);
switch (node->type) {
case CMARK_NODE_DOCUMENT:
if (entering) {
// Reset
currentX = sceneMarginX;
currentY = sceneMarginY;
headingLevel = 0;
listLevel = 0;
heighestHigh = 0;
bulletPointDynamicOffset = 0;
}
break;
case CMARK_NODE_BLOCK_QUOTE:
break;
case CMARK_NODE_LIST:
if (entering) {
listLevel++;
} else {
listLevel--;
if (listLevel < 0)
listLevel = 0;
}
if (listLevel == 0) {
// Reset X to be safe
currentX = sceneMarginX;
} else if (listLevel > 0) {
if (entering) {
currentX += listXOffset;
} else {
currentX -= listXOffset;
}
}
break;
case CMARK_NODE_ITEM:
// Line break for list items
currentY += heighestHigh;
// Reset heighest high (Y-axis)
heighestHigh = 0;
// Add bullet before text items
if (entering) {
//const QRectF rec = drawBullet();
//bulletPointDynamicOffset = rec.width() + 2.0; // + offset
currentX += bulletPointDynamicOffset;
} else {
currentX -= bulletPointDynamicOffset;
}
break;
case CMARK_NODE_HEADING:
if (entering) {
headingLevel = node->as.heading.level;
} else {
headingLevel = 0; // reset
}
// Move to left again
currentX = sceneMarginX;
// New heading
currentY += heighestHigh + headingHeightOffset;
// Reset heighest high (Y-axis)
heighestHigh = 0;
break;
case CMARK_NODE_CODE_BLOCK:
break;
case CMARK_NODE_HTML_BLOCK:
break;
case CMARK_NODE_CUSTOM_BLOCK:
break;
case CMARK_NODE_THEMATIC_BREAK:
break;
case CMARK_NODE_PARAGRAPH:
// Skip paragraph if listing is enabled
if (listLevel == 0) {
// Move to left again
currentX = sceneMarginX;
// New paragraph
currentY += heighestHigh + paragraphHeightOffset;
// Reset heighest high (Y-axis)
heighestHigh = 0;
}
break;
case CMARK_NODE_TEXT: {
printf("Text: %s\n", cmark_node_get_literal(node));
/*const QRectF rec = drawText(cmark_node_get_literal(node));
// Skip paragraph if listing is enabled
if (listLevel == 0) {
currentX += rec.width();
}
if (rec.height() > heighestHigh)
heighestHigh = rec.height();
*/
}
break;
case CMARK_NODE_LINEBREAK:
// Move to left again
currentX = sceneMarginX;
// Line break (no soft break)
currentY += heighestHigh;
// Reset heighest high (Y-axis)
heighestHigh = 0;
break;
case CMARK_NODE_SOFTBREAK:
// ignore
// Only insert a space between the words
currentX += wordSpacing;
break;
case CMARK_NODE_CODE:
break;
case CMARK_NODE_HTML_INLINE:
break;
case CMARK_NODE_CUSTOM_INLINE:
break;
case CMARK_NODE_STRONG:
//bold = entering;
break;
case CMARK_NODE_EMPH:
//italic = entering;
break;
case CMARK_NODE_LINK:
break;
case CMARK_NODE_IMAGE:
break;
case CMARK_NODE_FOOTNOTE_REFERENCE:
break;
case CMARK_NODE_FOOTNOTE_DEFINITION:
break;
default:
assert(false);
break;
}
}
bool RenderArea::on_draw(const Cairo::RefPtr<Cairo::Context>& cr)
{
Gtk::Allocation allocation = get_allocation();
const int width = allocation.get_width();
const int height = allocation.get_height();
const int rectangle_width = width;
const int rectangle_height = height / 2;
// Draw a black rectangle
cr->set_source_rgb(0.0, 0.0, 0.0);
draw_rectangle(cr, rectangle_width, rectangle_height);
// and some white text
cr->set_source_rgb(1.0, 1.0, 1.0);
draw_text(cr, rectangle_width, rectangle_height);
// flip the image vertically
// see http://www.cairographics.org/documentation/cairomm/reference/classCairo_1_1Matrix.html
// the -1 corresponds to the yy part (the flipping part)
// the height part is a translation (we could have just called cr->translate(0, height) instead)
// it's height and not height / 2, since we want this to be on the second part of our drawing
// (otherwise, it would draw over the previous part)
Cairo::Matrix matrix(1.0, 0.0, 0.0, -1.0, 0.0, height);
// apply the matrix
cr->transform(matrix);
// white rectangle
cr->set_source_rgb(1.0, 1.0, 1.0);
draw_rectangle(cr, rectangle_width, rectangle_height);
// black text
cr->set_source_rgb(0.0, 0.0, 0.0);
draw_text(cr, rectangle_width, rectangle_height);
draw_text(cr, 300, 110);
draw_text(cr, 200, 150);
draw_text(cr, 100, 240);
draw_text(cr, 400, 280);
return true;
}
void RenderArea::draw_rectangle(const Cairo::RefPtr<Cairo::Context>& cr,
int width, int height)
{
cr->rectangle(0, 0, width, height);
cr->fill();
}
void RenderArea::draw_text(const Cairo::RefPtr<Cairo::Context>& cr,
int rectangle_width, int rectangle_height)
{
// http://developer.gnome.org/pangomm/unstable/classPango_1_1FontDescription.html
Pango::FontDescription font;
font.set_family("Ubuntu");
font.set_weight(Pango::WEIGHT_BOLD);
if (rectangle_width < 150)
font.set_style(Pango::Style::STYLE_ITALIC);
// http://developer.gnome.org/pangomm/unstable/classPango_1_1Layout.html
auto layout = create_pango_layout("Hi there!");
layout->set_font_description(font);
int text_width;
int text_height;
//get the text dimensions (it updates the variables -- by reference)
layout->get_pixel_size(text_width, text_height);
// Position the text in the middle
cr->move_to((rectangle_width-text_width)/2, (rectangle_height-text_height)/2);
layout->show_in_cairo_context(cr);
}

38
src/render-area.h Normal file
View File

@ -0,0 +1,38 @@
#ifndef RENDER_AREA_H
#define RENDER_AREA_H
#include <gtkmm/drawingarea.h>
#include <cmark-gfm.h>
class RenderArea : public Gtk::DrawingArea
{
public:
RenderArea();
virtual ~RenderArea();
void renderDocument(cmark_node *root_node);
protected:
//Override default signal handler:
bool on_draw(const Cairo::RefPtr<Cairo::Context>& cr) override;
private:
int currentX;
int currentY;
int sceneMarginX;
int sceneMarginY;
int headingLevel;
int listLevel;
int wordSpacing;
int heighestHigh;
int paragraphHeightOffset;
int headingHeightOffset;
int listXOffset;
int bulletPointDynamicOffset;
void renderNode(cmark_node *node, cmark_event_type ev_type);
void draw_rectangle(const Cairo::RefPtr<Cairo::Context>& cr, int width, int height);
void draw_text(const Cairo::RefPtr<Cairo::Context>& cr, int rectangle_width, int rectangle_height);
};
#endif

View File

@ -1,20 +0,0 @@
#ifndef RENDER_INTERFACE_H
#define RENDER_INTERFACE_H
class Scene;
struct cmark_node;
/**
* \class RendererI Renderer Abstract Base Class
*/
class RendererI
{
public:
virtual ~RendererI() {};
// TODO: Combien those two sets to 1, create an abstract class for setting a scene in Qt and/or Imgui.
virtual void setScene(Scene *scene) = 0;// For Qt
virtual void setUnknownYet() = 0; // For Imgui
virtual void renderDocument(cmark_node *root, int width = 0) = 0;
};
#endif

View File

@ -1,7 +0,0 @@
#include "scene.h"
Scene::Scene(QObject *parent)
: QGraphicsScene(parent)
{
setSceneRect(QRectF(0, 0, 300, 300));
}

View File

@ -1,17 +0,0 @@
#ifndef SCENE_H
#define SCENE_H
#include <QGraphicsScene>
class Scene : public QGraphicsScene
{
Q_OBJECT
public:
explicit Scene(QObject *parent = 0);
private:
};
#endif