Skip to content

Instantly share code, notes, and snippets.

@cellularmitosis
Last active October 17, 2019 01:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cellularmitosis/100949865772a83561d634794fa2c749 to your computer and use it in GitHub Desktop.
Save cellularmitosis/100949865772a83561d634794fa2c749 to your computer and use it in GitHub Desktop.
Baby steps towards structured editing. Progress as of 2019/2/24.

Blog 2019/2/24

<- previous | index | next ->

Towards structured editing of code (part 1)

part 2 ->

Rationale

It is 2019 and the dominant programming workflow is still hand-editing text files! Let's start working towards a better future.

Initial plan:

  • Start small: a JSON or EDN visualizer
  • Then add basic editing operations to it
  • At no time is the programmer directly editing the raw text representation of the structure -- all edits are operations on the AST.

Let's look at an EDN fragment (a snippet of Clojure code):

screen shot 2019-02-24 at 5 38 22 pm

Text is merely one possible way to represent this structure. Many alternate representations are possible, for example:

  • symbols in white boxes
  • lists as blue boxes
  • vectors as green boxes

Mockup:

(made in google docs draw.io):

screen shot 2019-02-24 at 5 38 17 pm

Prototype

Let's try to write a program which can make something similar.

Attached is a rough pyqt5 script which renders a list of words in colored boxes:

screen shot 2019-02-25 at 12 30 48 am

That's my progress so far.

Next steps:

  • Render a group of words in a containing box (representing a list or vector)
  • Interactivity (a "selected" box based on keyboard input)
  • Editing individual nodes
#!/usr/bin/env python
# a pyqt script which draws words inside of rounded rectangles.
# see https://doc.qt.io/qt-5/qpainter.html
# see http://pyqt.sourceforge.net/Docs/PyQt5/api/qpen.html
import sys
import time
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
words = ["hello", "world", "these", "are", "words", "in", "boxes"]
# this function will be called on every paint event.
def on_paint(event, painter):
# fill the canvas with white
painter.fillRect(event.rect(), QBrush(Qt.white))
# paint the words
(x, y) = (20, 20)
paint_words(words, painter, x, y)
# paint the words in boxes, all in a single row.
def paint_words(words, painter, x, y):
pad = 5
# draw the words in boxes, all in a row.
for word in words:
(width, height) = paint_word(word, painter, x, y)
x += (width + pad)
# paint a word inside of a box, starting at (x, y)
# returns the (width, height) used to paint the word.
def paint_word(text, painter, x, y):
text_pad = 5
text_top_fudge = -2 # qt text seems to have extra padding on top.
corner_radius = 4
# calculate the size of the text
text_bounds = QFontMetrics(painter.font()).boundingRect(text)
# configure the box stroke
pen = QPen()
pen.setColor(Qt.red)
pen.setWidth(1)
painter.setPen(pen)
# configure the box fill
painter.setBrush(QBrush(QColor(255,192,192,255))) # light red
# draw the box
# note: the rounded corners come out a bit odd.
# see https://stackoverflow.com/questions/6507511/qt-round-rectangle-why-corners-are-different
painter.drawRoundedRect(
x,
y,
text_bounds.width() + (text_pad * 2),
text_bounds.height() + (text_pad * 2),
corner_radius,
corner_radius
)
# configure the text stroke
pen = QPen() # defaults to black
painter.setPen(pen)
# draw the text
painter.drawText(
x + text_pad,
y + text_bounds.height() + text_pad + text_top_fudge,
text
)
return (
text_bounds.width() + (text_pad * 2),
text_bounds.height() + (text_pad * 2)
)
# a widget which paints words inside of boxes
class WordPainter(QWidget):
def __init__(self, on_paint_fn):
QWidget.__init__(self)
self.on_paint_fn = on_paint_fn
# this gets called every time the widget needs to repaint (e.g. window resize)
def paintEvent(self, event):
then = time.time()
painter = QPainter()
painter.begin(self)
self.on_paint_fn(event, painter)
painter.end()
now = time.time()
elapsed = now - then
print "elapsed: %s" % elapsed
print "fps: %s" % (1.0/elapsed)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = WordPainter(on_paint)
window.show()
sys.exit(app.exec_())
@cellularmitosis
Copy link
Author

Initially I wanted to do this with using an OpenGL back-end, to get hardware acceleration, and things like zooming / panning for free. I started working with Dear ImGUI, but soon found that uses a lot of power (poor battery life for laptops). So I stepped away from "immediate mode" GUI and tried a "retained mode" GUI, via pyqt.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment