After playing with some code to try to make a canvas widget, I realised I was missing some important ideas. So I will try to write down a new set of thoughts.
One observation I made was that I should make better use of Python's basic types. For example, there is no point creating a "Colour" type when a tuple of (name,r,g,b,alpha,pixel) will do just as well. Similarly the standard python list should be used for a per-window (or per-pane) display list.
Also, input must be integrated with output. There is no point having a Canvas widget that renders stuff but doesn't accept input.
Finally, the display list could use a bit more structure than just panes containing random text and panes. There is an underlying ordering of most text, and it is worth while allowing that the be reflected usefully in the display list.
Other observations that are useful input to the design process:
-
Using Python call-backs for lazy object evaluation is a core design decision for this project. There should never be any call to do a lot of work in one hit. Instead, a piece of code should do a small or moderate amount of work, and leave incomplete parts of the result as objects that can cause further callbacks to do a bit more of the work.
For example, when parsing a large file, we might start with a lazy object (parsefile start (fileobject 0 END)) which is a call to "parsefile" setting the parse-state as "start" and the text to be parsed as the whole of a file, which might not even have been read yet.
When "parsefile" is called, it might parse the first paragraph, or function, or some amount of data, and return a structure that is partly parsed but contains further calls to "parsefile" with different parse-state and different text ranges.
Creating the display list should be done in exactly this lazy fashion, so there must be room in the structure to store descriptions of as-yet unrendered data, and a well defined mechanism for calling these descriptions.
-
Layout is affected by the rendered size of text, which is most apparent to the rendering engine. It seems tempting to perform a provisional layout creating a display list. We then get this sized by the rendering engine, and finally fine-tune the layout based on this size information.
This approach seems good for a basic text editor which isn't very fussy about layout and just wants the text to look OK on the screen, but doesn't seem so good for a document formatter which wants precise details so the document can be rendered the same way on the display or on a printer.
I think that, with the document formatter in mind, we want to keep these two functions: sizing and drawing, separate. They can certainly use some common data structures, such as a list of characters that are to be juxtaposed one after the other.
-
The parameters to the rendering function need to include not only the document and a style, but also
- A current cursor position in the document
- A scroll-window into the document
So where does that leave us?
Maybe not so very different from where we started except for the lazy cells.
For editable text (As opposed to e.g. labels) we really want individual letters in the display list so that a mouse-click can find them easily. Each letter (glyph) needs a size and a position relative to the start of the list.
We don't want to have to re-render the whole page after any change, but for generality we need to regenerate the display list. We therefore need a 'current' display list and a 'new' display list that we compare looking for differences that need to be rendered.
The 'scroll-window' is a problem without an immediately obvious solution.
Using the scroll-bar we want to be able to select a point some fraction of the way through the document (whether this point is where the display starts or ends or the middle is a separate question). As the document has more structure than simple text, it may not be meaningful to simply count lines.
We might be able to require the parsing process to count or estimate some line-count-like value, but it could be that the width of the display substantially affects the real line count. Possibly the linecount is based on an estimate (80 char lines) and we don't worry about the error. After all, dragging the scoll bar doesn't make for precise positioning - you need line-steps or page-steps for that, and these can be done based on precise knowledge of recent rendering.
So, back the the "separate question" - Exactly what value to we give when we record a scroll-bar position? Document location at top of page? at bottom? at middle? Or maybe where on the page the cursor is?
It think the later is most general. We make it a bit more general by saying that the cursor position includes some rendering context, with the precise meaning of the context dependant on the rendering function. For simple text it might be the screen line, and the amount of horizontal scroll needed.
The rendering function (well, the display-list-generating function) should start with the text around the cursor, render that, and see how much space is left for more context. It will quite possibly take a largish section of the document, certainly a line, possible a paragraph, maybe even a page. Once this is rendered into a display list, some of the list might be found to be outside the window and so it will have to be discarded.
How do we store a point in the document? Just the leaf node is not sufficient was walking back up the tree and understanding context would be awkward. Possibly a list of nodes on the path down the document tree to the leaf, with indexes along the way as hints - if they are wrong it doesn't matter.
We would probably want to flag any node that is listed on a position so that if the node is changed, we can hunt through all positions to find it and make appropriate modifications based on hints in the position.
The Design of the Display List
The key functionality that the display list must provide is:
- To be able to easily render part or all of the display list
- To be able to map from a screen location back to a document node
The display list is made up of display objects.
Each display object contains:
- x,y co-ordinates relative to a containing object.
- u,d,l,r distances of bounding-box edge from origin
- matrix for linear transforms on the content
- no-overlap flag, set if contents are known not to overlap
- relative depth, aka layer number.
- text-string - used to render text
- object-list - used to gather subobjects
As well as display objects, the object list can contain:
- Font/face and selections which apply to all subsequent text in the list.
- Foreground and background colour setting
- border information (width, colour, shading guide)
These should appear before any text or display objects, and apply to the whole display list.
Redisplay
The image needs to be redisplayed if some area has been damaged or the document has changed. For a change in the document, we first calculate the area (possibly) damaged by that change, and then perform a redisplay of damaged areas.When the document is changed, the display module asks for a new display list. This display list may well share sub-lists with the original, and may contain function rather than data nodes which must be called as they are found.
The new and old display lists are walked in unison. When a significant difference is found, the areas covering the before and after objects are added to the damaged region.
In some cases, this might be the whole display. In others it might be a single character.
Once we have a current display list and a damaged area, we blank all rectangles in the damaged area, and the walk the display tree finding any object with a bounding rectangle that intersects the damaged area, and we render it clipped to the damaged area. When a list contains possibly overlapping objects, we make a first pass to find how many layers there are, and then another pass for each layer.
As we walk the tree for display, we keep track (on a stack) of the current origin, matrix, font, and foreground colour. Background colour and border information do not need to be tracked, background because it is already drawn, and border because it is not inherited.
point to node mapping
When a UI event such as a mouse click identified a particlar point in the display, we need to make it to a node in the document. This simply involves a recursive walk, mapping the window relative co-ordinates to object-relative and testing for inclusion. The matrix must be applied in reverse before checking if a co-ordinate is in the region. When there are possible overlapping objects, we first find a list of layers and then make one pass for each layer. Though we could optimise that a bit...If the display-object that is found to contain the point doesn't have an associated document location, we find the closest known location up the tree, and provide a count of how many objects later in the list the final object was.
This information is passed to... some function.
