Commits

Anonymous committed 3014e9f

Gui classes for neovim-qt

- Added all gui classes in src/gui
- NeovimQt::Gui is a gui event handler that processes
Neovim events and passes them on to the windows
- NeovimQt::BaseGui is the abstract class from which other
Guis can inherit
- NeovimQt::WindowWidget is a Neovim Vim implementation based
on QWidget
- NeovimQt::BaseWindow is the abstract class to use for custom
window implementations

Comments (0)

Files changed (11)

 add_library(neovim-qt ${NEOVIM_QT_SOURCES})
 target_link_libraries(neovim-qt Qt5::Network ${MSGPACK_LIBRARIES})
 
+
+add_subdirectory(gui)
+

src/gui/CMakeLists.txt

+
+include_directories(${CMAKE_CURRENT_SOURCE_DIR}/..)
+
+set(GUI_SOURCES main.cpp basegui.cpp gui.cpp windowwidget.cpp basewindow.cpp)
+add_executable(neovim-gui ${GUI_SOURCES})
+target_link_libraries(neovim-gui Qt5::Widgets Qt5::Network ${MSGPACK_LIBRARIES} neovim-qt)
+
+
+#include "gui.h"
+
+namespace NeovimQt {
+
+
+BaseGui::BaseGui(NeovimConnector *connector, QObject *parent)
+:QObject(parent), m_nc(connector)
+{
+	connect(m_nc, &NeovimConnector::ready,
+			this, &BaseGui::neovimReady);
+	connect(m_nc, &NeovimConnector::notification,
+			this, &BaseGui::neovimNotification);
+	//connect(m_nc->neovimObject(), &Neovim::on_vim_get_windows);
+}
+
+void BaseGui::neovimReady()
+{
+	m_nc->neovimObject()->vim_subscribe("redraw:*");
+	m_nc->neovimObject()->vim_subscribe("redraw:insert_line");
+	m_nc->neovimObject()->vim_subscribe("redraw:delete_line");
+	m_nc->neovimObject()->vim_subscribe("redraw:status_line");
+	m_nc->neovimObject()->vim_subscribe("redraw:ruler");
+	m_nc->neovimObject()->vim_subscribe("redraw:cursor");
+	m_nc->neovimObject()->vim_subscribe("redraw:layout");
+	m_nc->neovimObject()->vim_subscribe("redraw:tabs");
+	m_nc->neovimObject()->vim_subscribe("redraw:update_line");
+	m_nc->neovimObject()->vim_subscribe("redraw:foreground_color");
+	m_nc->neovimObject()->vim_subscribe("redraw:background_color");
+	m_nc->neovimObject()->vim_subscribe("redraw:win_end");
+	// Ask Neovim to redraw everything for us
+	m_nc->neovimObject()->vim_request_screen();
+}
+
+void BaseGui::handleRedrawCursor(const QVariantMap& m)
+{
+	if ( !m.contains("col") || !m.contains("row") || 
+			!m.value("col").canConvert<quint64>() ||
+			!m.value("row").canConvert<quint64>()) {
+		qWarning() << "Invalid event for redraw:cursor" << m;
+		return;
+	}
+
+	uint64_t col = m.value("col").toLongLong();
+	uint64_t row = m.value("row").toLongLong();
+
+	emit redrawCursor(col, row);
+}
+void BaseGui::handleRedrawRuler(const QVariantMap& m)
+{
+	// FIXME: do something w/ the ruler
+}
+
+void BaseGui::handleRedrawUpdateLine(const QVariantMap& m)
+{
+	if (!m.value("window_id").canConvert<quint64>()) {
+		qWarning() << "Received redraw:update_line event with invalid window_id";
+		return;
+	}
+	uint64_t window = m.value("window_id").toLongLong();
+
+	if (!m.value("row").canConvert<quint64>()) {
+		qWarning() << "Received redraw:update_line event with invalid row";
+		return;
+	}
+	uint64_t row = m.value("row").toLongLong();
+
+	if (!m.value("line").type() == QMetaType::QVariantList ) {
+		qWarning() << "Received redraw:update_line event without a line";
+		return;
+	}
+
+	QVariantMap attrs;
+	if (m.contains("attributes")) {
+		// attributes are OPTIONAL
+		attrs = m.value("attributes").toMap();
+	}
+
+	// FIXME: consider moving this out of each method
+	if (hasWindow(window)) {
+		getWindow(window)->updateLine(row, m.value("line").toList(), attrs);
+	} else {
+		qWarning() << "Received event" << __func__ << "for unknown window";
+	}
+}
+
+void BaseGui::handleRedrawInsertLine(const QVariantMap& m)
+{
+	if (!m.value("window_id").canConvert<quint64>()) {
+		qWarning() << "Received redraw:insert_line event with invalid window_id";
+		return;
+	}
+	uint64_t window = m.value("window_id").toLongLong();
+
+	if (!m.value("row").canConvert<quint64>()) {
+		qWarning() << "Received redraw:insert_line event with invalid row";
+		return;
+	}
+	uint64_t row = m.value("row").toLongLong();
+
+	if (!m.value("count").canConvert<quint64>()) {
+		qWarning() << "Received redraw:insert_line event with invalid count";
+		return;
+	}
+	uint64_t count = m.value("count").toLongLong();
+
+	// FIXME
+	if (hasWindow(window)) {
+		getWindow(window)->insertLine(row, count);
+	}
+}
+
+void BaseGui::handleRedrawDeleteLine(const QVariantMap& m)
+{
+	if (!m.value("window_id").canConvert<quint64>()) {
+		qWarning() << "Received redraw:insert_line event with invalid window_id";
+		return;
+	}
+	uint64_t window = m.value("window_id").toLongLong();
+
+	if (!m.value("row").canConvert<quint64>()) {
+		qWarning() << "Received redraw:delete_line event with invalid row";
+		return;
+	}
+	uint64_t row = m.value("row").toLongLong();
+
+	if (!m.value("count").canConvert<quint64>()) {
+		qWarning() << "Received redraw:delete_line event with invalid count";
+		return;
+	}
+	uint64_t count = m.value("count").toLongLong();
+
+	// FIXME
+	if (hasWindow(window)) {
+		getWindow(window)->deleteLine(row, count);
+	}
+}
+
+/**
+ * Neovim informs us of the window layout and the dimensions of each
+ * window
+ *
+ * To implement your own GUI widgets/layout you should override this
+ * method @see setWindow
+ */
+
+
+void BaseGui::handleRedrawForegroundColor(const QVariantMap& m)
+{
+	// FIXME
+	foreach(uint64_t id, windows()) {
+		getWindow(id)->setForegroundColor(m.value("color").toString());
+	}
+}
+
+void BaseGui::handleRedrawBackgroundColor(const QVariantMap& m)
+{
+	// FIXME
+	foreach(uint64_t id, windows()) {
+		getWindow(id)->setBackgroundColor(m.value("color").toString());
+	}
+}
+
+void BaseGui::handleRedrawWinEnd(const QVariantMap& m)
+{
+	if (!m.value("window_id").canConvert<uint64_t>()) {
+		qWarning() << "Received redraw:win_end event with invalid window_id";
+		return;
+	}
+	uint64_t window_id = m.value("window_id").toLongLong();
+
+	if (!m.value("row").canConvert<uint64_t>()) {
+		qWarning() << "Received redraw:win_end event with invalid row";
+		return;
+	}
+	uint64_t row = m.value("row").toLongLong();
+
+	if (!m.value("endrow").canConvert<uint64_t>()) {
+		qWarning() << "Received redraw:win_end event with invalid endrow";
+		return;
+	}
+	uint64_t endrow = m.value("endrow").toLongLong();
+
+	if (!m.value("marker").canConvert<QString>()) {
+		qWarning() << "Received redraw:win_end event with invalid marker";
+		return;
+	}
+	QString marker = m.value("marker").toString();
+
+	if (!m.value("fill").canConvert<QString>()) {
+		qWarning() << "Received redraw:win_end event with invalid fill";
+		return;
+	}
+	QString fill = m.value("fill").toString();
+
+	// FIXME
+	if (hasWindow(window_id)) {
+		getWindow(window_id)->windowEnded(row, endrow, marker, fill);
+	}
+}
+
+void BaseGui::neovimNotification(const QByteArray& name, const QVariant& args)
+{
+	if (name == "redraw:cursor") {
+		handleRedrawCursor(args.toMap());
+		return;
+	} else if (name == "redraw:ruler") {
+		handleRedrawRuler(args.toMap());
+		return;
+	} else if (name == "redraw:update_line") {
+		handleRedrawUpdateLine(args.toMap());
+		return;
+	} else if (name == "redraw:insert_line") {
+		handleRedrawInsertLine(args.toMap());
+		return;
+	} else if (name == "redraw:delete_line") {
+		handleRedrawDeleteLine(args.toMap());
+		return;
+	} else if (name == "redraw:layout") {
+		handleRedrawLayout(args.toMap());
+		return;
+	} else if (name == "redraw:foreground_color") {
+		handleRedrawForegroundColor(args.toMap());
+		return;
+	} else if (name == "redraw:background_color") {
+		handleRedrawBackgroundColor(args.toMap());
+		return;
+	} else if (name == "redraw:win_end") {
+		handleRedrawWinEnd(args.toMap());
+		return;
+	} else {
+		qWarning() << "Received unknown event" << name;
+	}
+}
+
+} // Namespace
+
+#ifndef NEOVIM_QT_GUI_BASEGUI
+#define NEOVIM_QT_GUI_BASEGUI
+
+#include <neovimconnector.h>
+#include "basewindow.h"
+
+namespace NeovimQt {
+
+class BaseGui: public QObject
+{
+	Q_OBJECT
+public:
+	BaseGui(NeovimConnector *n, QObject *parent=0);
+
+signals:
+	void redrawCursor(uint64_t col, uint64_t row);
+	void updateLine(uint64_t window, uint64_t row, const QVariantList& line, const QVariantMap& attrs);
+	void insertLine(uint64_t window, uint64_t row_index, uint64_t count);
+	void deleteLine(uint64_t window, uint64_t row_index, uint64_t count);
+	void redrawLayout(uint64_t window, uint64_t height, uint64_t width);
+	void backgroundColor(const QString&);
+	void foregroundColor(const QString&);
+	void windowEnded(uint64_t window_id, uint64_t row, uint64_t endrow, const QString& marker, const QString& fill);
+
+protected:
+	void handleRedrawCursor(const QVariantMap&);
+	void handleRedrawRuler(const QVariantMap&);
+	void handleRedrawUpdateLine(const QVariantMap&);
+	void handleRedrawInsertLine(const QVariantMap&);
+	void handleRedrawDeleteLine(const QVariantMap&);
+	void handleRedrawForegroundColor(const QVariantMap&);
+	void handleRedrawBackgroundColor(const QVariantMap&);
+	void handleRedrawWinEnd(const QVariantMap&);
+
+	virtual void handleRedrawLayout(const QVariantMap&)=0;
+
+	virtual bool hasWindow(uint64_t id)=0;
+	virtual BaseWindow* getWindow(uint64_t id)=0;
+	virtual QList<uint64_t> windows()=0;
+
+protected slots:
+	void neovimReady();
+	void neovimNotification(const QByteArray &name, const QVariant& args);
+
+private:
+	NeovimConnector *m_nc;
+
+};
+
+} // Namespace
+
+#endif

src/gui/basewindow.cpp

+#include "basewindow.h"
+#include <QDebug>
+
+namespace NeovimQt {
+
+/**
+ * Neovim sends us a list of attributes and a text range where they
+ * should be applied - this method breaks down the ranges into a map
+ * between each char position and the attributes to be applied
+ *
+ *     attrsToMap({attr:[range_pos]}) -> [[attr1,attr2], ...]
+ *
+ * You can use this if you need to do char-by-char rendering
+ */
+QList<QSet<QString> > BaseWindow::attrsToMap(const QString& text, const QVariantMap& attrs)
+{
+	QList<QSet<QString> > attrMap;
+	for (int j=0; j<text.size(); j++) {
+		attrMap.append(QSet<QString>());
+	}
+
+	for (int i=0; i<attrs.keys().size(); i++) {
+		QVariantList targets = attrs.value(attrs.keys().at(i)).toList();
+		foreach(QVariant target, targets) {
+			// Either a range [1,2) or an integer (1) == [1,2)
+			uint64_t start, end;
+			if (target.canConvert<quint64>()) {
+				// FIXME: Is this the right way
+				start = target.toLongLong();
+				end = start + 1;
+			} else if ((QMetaType::Type)target.type() == QMetaType::QVariantList &&
+					target.toList().size() == 2 &&
+					target.toList().at(0).canConvert<quint64>() &&
+					target.toList().at(1).canConvert<quint64>()) {
+
+				start = target.toList().at(0).toLongLong();
+				end = target.toList().at(1).toLongLong();
+			} else {
+				qWarning() << "Invalid line attribute";
+				continue;
+			}
+
+			for (uint64_t j=0; j<(uint64_t)text.size(); j++) {
+				if (j >= start && j < end) {
+					attrMap[j].insert(attrs.keys().at(i));
+				}
+			}
+		}
+	}
+	return attrMap;
+}
+
+}
+#ifndef NEOVIM_QT_GUI_BASEWINDOW
+#define NEOVIM_QT_GUI_BASEWINDOW
+
+#include <QSet>
+#include <QVariant>
+#include <QString>
+
+
+namespace NeovimQt {
+
+class BaseWindow
+{
+
+public:
+
+	static QList<QSet<QString> > attrsToMap(const QString& text, const QVariantMap& attrs);
+
+	virtual uint64_t windowId()=0;
+	virtual void redrawCursor(uint64_t col, uint64_t row)=0;
+	virtual void updateLine(uint64_t row, const QVariantList& line,
+		 		const QVariantMap& attrs)=0;
+	virtual void insertLine(uint64_t row_index, uint64_t count)=0;
+	virtual void deleteLine(uint64_t row_index, uint64_t count)=0;
+	virtual void redrawLayout(uint64_t height, uint64_t width)=0;
+	virtual void setForegroundColor(const QString&)=0;
+	virtual void setBackgroundColor(const QString&)=0;
+	virtual void windowEnded(uint64_t row, uint64_t endrow,
+			const QString& marker, const QString& fill)=0;
+};
+
+} // namespace
+
+#endif
+#include "gui.h"
+
+#include "windowwidget.h"
+#include <QHBoxLayout>
+#include <QVBoxLayout>
+
+namespace NeovimQt {
+
+Gui::Gui(NeovimConnector *n, QObject *parent)
+:BaseGui(n, parent)
+{
+	// FIXME: root widget, margins, etc
+	m_widget = new QWidget();
+	m_root_layout = new QVBoxLayout(m_widget);
+}
+
+void Gui::handleRedrawLayout(const QVariantMap& m)
+{
+	QString type = m.value("type").toString();
+	// FIXME: remove layout
+	/*
+	if (type == "column") {
+		m_root_layout = new QVBoxLayout(m_widget);
+		foreach(const QVariant v, m.value("children").toList()) {
+			handleRedrawLayout(v.toMap(), m_root_layout);
+		}
+	} else if (type == "row") {
+		m_root_layout = new QHBoxLayout(m_widget);
+		foreach(const QVariant v, m.value("children").toList()) {
+			handleRedrawLayout(v.toMap(), m_root_layout);
+		}
+	} else if (type == "leaf") {
+		handleRedrawLayout(m, m_root_layout);	
+	}*/
+
+	handleRedrawLayout(m, m_root_layout);	
+	m_widget->show();
+}
+
+/**
+ * Recursive layout builder
+ */
+void Gui::handleRedrawLayout(const QVariantMap& m, QBoxLayout *layout)
+{
+	QString type = m.value("type").toString();
+	if (type == "column") {
+		QVBoxLayout *column = new QVBoxLayout();
+		foreach(const QVariant v, m.value("children").toList()) {
+			handleRedrawLayout(v.toMap(), column);
+		}
+		layout->addLayout(column);
+	} else if (type == "row") {
+		QHBoxLayout *row = new QHBoxLayout();
+		foreach(const QVariant v, m.value("children").toList()) {
+			handleRedrawLayout(v.toMap(), row);
+		}
+		layout->addLayout(row);
+	} else if (type == "leaf") {
+		// We are a leaf - i.e. a window
+		if (!m.value("window_id").canConvert<quint64>()) {
+			qWarning() << "Received redraw:layout event with invalid window_id";
+			return;
+		}
+		uint64_t window = m.value("window_id").toLongLong();
+
+		if (!m.value("height").canConvert<quint64>()) {
+			qWarning() << "Received redraw:layout event with invalid height";
+			return;
+		}
+		uint64_t height = m.value("height").toLongLong();
+
+		if (!m.value("width").canConvert<quint64>()) {
+			qWarning() << "Received redraw:layout event with invalid width";
+			return;
+		}
+		uint64_t width = m.value("width").toLongLong();
+
+		WindowWidget *w;
+		if (hasWindow(window)) {
+			w = m_windows.value(window);
+		} else {
+			w = new WindowWidget(this, window, m_widget);
+			m_windows.insert(window, w);
+		}
+		layout->addWidget(w);
+		w->redrawLayout(height, width);
+	} else {
+		qWarning() << "Unrecognised layout node: " << type;
+	}
+}
+
+BaseWindow* Gui::getWindow(uint64_t window_id)
+{
+	return m_windows.value(window_id);
+}
+
+bool Gui::hasWindow(uint64_t window_id)
+{
+	return m_windows.contains(window_id);
+}
+
+QList<uint64_t> Gui::windows()
+{
+	return m_windows.keys();
+}
+
+} //neovim
+#ifndef NEOVIM_QT_GUI_GUI
+#define NEOVIM_QT_GUI_GUI
+
+#include "basegui.h"
+#include <QBoxLayout>
+
+namespace NeovimQt {
+
+class WindowWidget;
+class Gui: public BaseGui
+{
+public:
+	Gui(NeovimConnector *n, QObject *parent=0);
+
+protected:
+	virtual void handleRedrawLayout(const QVariantMap&);
+	virtual bool hasWindow(uint64_t id);
+	virtual BaseWindow* getWindow(uint64_t id);
+	virtual QList<uint64_t> windows();
+
+private:
+	void handleRedrawLayout(const QVariantMap& arg, QBoxLayout *);
+	// FIXME
+	QWidget *m_widget;
+	QBoxLayout *m_root_layout;
+	QHash<uint64_t,WindowWidget*> m_windows;
+};
+} // namespace;
+
+#endif
+#include <QApplication>
+#include <neovimconnector.h>
+#include <QLocalSocket>
+#include "gui.h"
+
+int main(int argc, char **argv)
+{
+	QApplication app(argc, argv);
+	QLocalSocket s;
+	s.connectToServer(QLatin1String("/tmp/neovim"));
+	NeovimQt::NeovimConnector c(&s);
+
+	NeovimQt::Gui g(&c);
+
+	return app.exec();
+}

src/gui/windowwidget.cpp

+#include "windowwidget.h"
+#include <QPainter>
+#include <QPaintEvent>
+
+namespace NeovimQt {
+
+WindowWidget::WindowWidget(Gui *g, uint64_t window_id, QWidget *parent)
+:QWidget(parent), m_gui(g), 
+	m_rows(0), m_cols(0), m_window_id(window_id),
+	m_foreground(Qt::black),m_background(Qt::white)
+{
+	// Set default font
+	QFont f;
+	f.setStyleStrategy(QFont::StyleStrategy(QFont::PreferDefault | QFont::ForceIntegerMetrics) );
+	f.setStyleHint(QFont::TypeWriter);
+	f.setFamily("Monospace");
+	f.setFixedPitch(true);
+	f.setPointSize(10);
+	f.setKerning(false);
+	f.setFixedPitch(true);
+
+	m_font = f;
+	m_fm = new QFontMetrics(m_font);
+
+	setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
+
+	setAttribute(Qt::WA_OpaquePaintEvent, true);
+	setAttribute(Qt::WA_StaticContents, true);
+}
+
+void WindowWidget::insertLine(uint64_t row, uint64_t count)
+{
+	PaintOp p(INSERT_LINE);
+	p.rect = QRect(0, row*m_fm->height(),
+			m_cols*m_fm->width("W"), m_rows*m_fm->height());
+	p.pos = QPoint(0, count*m_fm->height());
+	queuePaintOp(p);
+}
+
+void WindowWidget::redrawCursor(uint64_t col, uint64_t row)
+{
+
+}
+
+void WindowWidget::updateLine(uint64_t row, const QVariantList& line, const QVariantMap& attrs)
+{
+	QString text;
+	QList<QSet<QString> > attrMap;
+	if (line.size() == 0) {
+		text = "";
+	} else {
+		// Build final string and attribute map
+		foreach(QVariant v, line) {
+			QVariantMap map = v.toMap();
+			text += map.value("content").toString();
+		}
+		attrMap = attrsToMap(text, attrs);
+	}
+
+	PaintOp bgp(CLEAR_RECT);
+	bgp.rect = QRect(0, row*m_fm->height(),
+			m_cols*m_fm->width("W"), (row+1)*m_fm->height());
+	queuePaintOp(bgp);
+
+	if (attrMap.isEmpty()) {
+		// Paint line with no formatting
+		queueUpdateLine(text, row, 0);
+		return;
+	}
+	
+	int startSection = 0;
+	// Split this into multiple paint ops
+	int pos;
+	for (pos=0; pos<text.size(); pos++) {
+		if (pos == 0 || attrMap[pos] == attrMap[pos-1]) {
+			continue;
+		}
+		queueUpdateLine(text.mid(startSection, pos-startSection),
+				row, startSection, attrMap[pos-1]);
+		startSection = pos;
+	}
+	queueUpdateLine(text.mid(startSection, pos-startSection),
+			row, startSection, attrMap[pos-1]);
+}
+
+/**
+ * A helper method to push text paint operations with
+ * attributes
+ */
+void WindowWidget::queueUpdateLine(const QString& text, 
+		uint64_t row, uint64_t col, const QSet<QString>& attrs)
+{
+	PaintOp p(UPDATE_LINE);
+	p.text = text;
+	p.pos = QPoint(col*m_fm->width("W"), row*m_fm->height()+m_fm->ascent());
+	p.rect = QRect(col*m_fm->width("W"), row*m_fm->height(),
+			(text.size())*m_fm->width("W"), m_fm->height());
+	p.font = m_font;
+
+	foreach(QString name, attrs) {
+		if (name == "bold") {
+			p.font.setBold(true);
+		} else if (name.startsWith("fg:")) {
+			QColor c(name.mid(3));
+			if (c.isValid()) {
+				p.fg_color = c;
+			}
+		} else if (name.startsWith("bg:")) {
+			QColor c(name.mid(3));
+			if (c.isValid()) {
+				p.bg_color = c;
+			}
+		} else if (name == "underline") {
+			p.font.setUnderline(true);
+		} else if (name == "italic") {
+			p.font.setItalic(true);
+		} else {
+			qWarning() << "Unsupported text attribute" << name;
+		}
+	}
+
+	queuePaintOp(p);
+}
+
+void WindowWidget::deleteLine(uint64_t row, uint64_t count)
+{
+	PaintOp p(DELETE_LINE);
+	p.rect = QRect(0, row*m_fm->height(),
+			m_cols*m_fm->width("W"), m_rows*m_fm->height());
+	p.pos = QPoint(0, -count*m_fm->height());
+	queuePaintOp(p);
+}
+
+void WindowWidget::redrawLayout(uint64_t height, uint64_t width)
+{
+	// TODO: there are cases when we cannot resize!
+	m_rows = height;
+	// FIXME: set width correctly
+	m_cols = width;
+	setFixedSize(m_cols*m_fm->width("W"), m_rows*m_fm->height());
+	updateGeometry();
+	show();
+}
+
+QSize WindowWidget::sizeHint() const
+{
+	return QSize(m_cols*m_fm->width("W"), m_rows*m_fm->height());
+}
+
+void WindowWidget::paintEvent ( QPaintEvent *ev )
+{
+	QPainter painter(this);
+	while ( !m_ops.isEmpty() ) {
+		painter.save();
+		PaintOp op = m_ops.dequeue();
+
+		switch( op.type ) {
+		case INSERT_LINE:
+		case DELETE_LINE:
+			// Insert/Delete line just scroll the canvas
+			painter.restore();
+			painter.end();
+			this->scroll(op.pos.x(), op.pos.y(), op.rect);
+			painter.begin(this);
+			continue;
+		case UPDATE_LINE:
+			// FIXME: see CLEAR_RECT
+			if (op.bg_color.isValid()) {
+				painter.fillRect(op.rect, op.bg_color);
+			}
+			if (op.fg_color.isValid()) {
+				painter.setPen(op.fg_color);
+			} else {
+				painter.setPen(m_foreground);
+			}
+			painter.setFont(op.font);
+			painter.drawText( op.pos, op.text);
+			break;
+		case CLEAR_RECT:
+			painter.fillRect(op.rect, m_background);
+			break;
+		default:
+			qWarning() << "Unsupported paint op" << op.type;
+		}
+		painter.restore();
+	}
+}
+
+/**
+ * Queue a painting operation on the widget
+ */
+void WindowWidget::queuePaintOp(PaintOp op)
+{
+	m_ops.enqueue(op);
+	if (op.rect.isValid()) {
+		update(op.rect);
+	}
+}
+
+void WindowWidget::setBackgroundColor(const QString& name)
+{
+	QColor c = QColor(name);
+	if (c.isValid()) {
+		m_background = c;
+	}
+}
+
+void WindowWidget::setForegroundColor(const QString& name)
+{
+	QColor c = QColor(name);
+	if (c.isValid()) {
+		m_foreground = c;
+	}
+}
+
+/**
+ * Called when a window has scrolled to the end and the empty rows need to
+ * be cleared - Neovim places a _marker_ at the line start and fills the
+ * remaining positions with _fill_
+ */
+void WindowWidget::windowEnded(uint64_t row, uint64_t endrow, const QString& marker, const QString& fill)
+{
+	QString text = marker;
+	for (uint64_t i=1; i<m_cols; i++ ) {
+		text += fill;
+	}
+	for (uint64_t i=row; i<endrow; i++) {
+		// FIXME: clean this up please
+		PaintOp bgp(CLEAR_RECT);
+		bgp.rect = QRect(0, i*m_fm->height(),
+				m_cols*m_fm->width("W"), (i+1)*m_fm->height());
+		queuePaintOp(bgp);
+		queueUpdateLine(text, i, 0);
+	}
+}
+
+void WindowWidget::resizeEvent(QResizeEvent *ev)
+{
+	if (!ev->oldSize().isValid()) {
+		// When first resizing fill up with the background
+		// color
+		PaintOp p(CLEAR_RECT);
+		p.rect = rect();
+		queuePaintOp(p);
+		return;
+	}
+
+	// Paint margins outside the Neovim window
+	int dx = ev->size().width() - (int)m_cols*m_fm->width("W");
+	int dy = ev->size().height() - (int)m_rows*m_fm->height();
+
+	if (dx > 0) {
+		PaintOp bgp(CLEAR_RECT);
+		bgp.rect = QRect(m_cols*m_fm->width("W"), 0,
+			ev->size().width(), m_rows*m_fm->height());
+		queuePaintOp(bgp);
+	}
+	if (dy > 0) {
+		PaintOp bgp(CLEAR_RECT);
+		bgp.rect = QRect(0, m_rows*m_fm->height(),
+			m_cols*m_fm->width("W"), ev->size().height());
+		queuePaintOp(bgp);
+	}
+	if (dx > 0 && dy > 0) {
+		PaintOp bgp(CLEAR_RECT);
+		bgp.rect = QRect(m_cols*m_fm->width("W"), m_rows*m_fm->height(),
+			ev->size().width(), ev->size().height());
+		queuePaintOp(bgp);
+	}
+}
+
+} // Namespace
+

src/gui/windowwidget.h

+#ifndef NEOVIM_QT_WINDOWWIDGET
+#define NEOVIM_QT_WINDOWWIDGET
+
+#include <QWidget>
+#include <QQueue>
+#include "gui.h"
+#include "basewindow.h"
+
+namespace NeovimQt {
+
+class WindowWidget: public QWidget, public BaseWindow
+{
+	Q_OBJECT
+	Q_ENUMS(PaintOpType)
+public:
+	WindowWidget(Gui *g, uint64_t window_id, QWidget *parent=0);
+
+	enum PaintOpType {CLEAR_ALL, DELETE_LINE, INSERT_LINE, UPDATE_LINE, CLEAR_RECT, NOOP};
+	class PaintOp {
+	public:
+		PaintOp(PaintOpType t=NOOP) {type=t;}
+		PaintOpType type;
+		QPoint pos;
+		QRect rect;
+		QString text;
+		QFont font;
+		QColor fg_color, bg_color;
+	};
+
+	virtual QSize sizeHint() const;
+public:
+	virtual uint64_t windowId() {return m_window_id;}
+	void redrawCursor(uint64_t col, uint64_t row);
+	virtual void updateLine(uint64_t row, const QVariantList& line, const QVariantMap& attrs);
+	virtual void insertLine(uint64_t row_index, uint64_t count);
+	virtual void deleteLine(uint64_t row_index, uint64_t count);
+	virtual void redrawLayout(uint64_t height, uint64_t width);
+	virtual void setForegroundColor(const QString&);
+	virtual void setBackgroundColor(const QString&);
+	virtual void windowEnded(uint64_t row, uint64_t endrow, const QString& marker, const QString& fill);
+
+protected:
+	void queuePaintOp(PaintOp);
+	void paintEvent(QPaintEvent *ev);
+	void resizeEvent(QResizeEvent *ev);
+	void queueUpdateLine(const QString& text, uint64_t row, 
+			uint64_t col, const QSet<QString>& attrs=QSet<QString>());
+
+private:
+	QQueue<PaintOp> m_ops;
+	Gui *m_gui;
+	QFont m_font;
+	QFontMetrics *m_fm;
+	uint64_t m_rows, m_cols, m_window_id;
+	QColor m_foreground, m_background;
+	QPixmap m_canvas;
+};
+
+
+} // Namespace
+
+#endif