Skip to content

Design 2D

Randall C. O'Reilly edited this page Jan 25, 2024 · 1 revision
  • There are three main types of 2D nodes:

    • Viewport2D nodes that manage their own oswin.Image bitmap and can upload that directly to the oswin.Texture that then uploads directly to the oswin.Window. The parent Window has a master Viewport2D that backs the entire window, and is what most Widget's render into.
      • Popup Dialog and Menu's have their own viewports that are layered on top of the main window viewport.
      • SVG and its subclass Icon are containers for SVG-rendering nodes.
    • Widget nodes that use the full CSS-based styling (e.g., the Box model etc), are typically placed within a Layout -- they use units system with arbitrary DPI to transform sizes into actual rendered dots (term for actual raw resolution-dependent pixels -- "pixel" has been effectively co-opted as a 96dpi display-independent unit at this point). Widgets have non-overlapping bounding boxes (BBox).
    • SVG rendering nodes that directly set properties on the Paint object and typically have their own geometry etc -- they should be within a parent SVG viewport, and their geom units are determined entirely by the transforms etc and we do not support any further unit specification -- just raw float values.
  • Rendering is done via Steve Wiley's rasterx package: https://github.com/srwiley/rasterx, which in turn can use either of the following packages for final rasterization:

  • We adapted the Painter level API from: https://github.com/fogleman/gg, and separated that API from the RenderState object that contains all the actual state that is updated -- this lives in the Viewport2D object. Also converted the gg system to float32 instead of 64, using the geom.go Vec2D core element. Note that the github.com/go-gl/mathgl/mgl32/ math elements (vectors, matricies) which build on the basic golang.org/x/image/math/f32 do not have appropriate 2D rendering transforms etc.

  • Text rendering (all in gi/text.go) was written from the ground-up to support full "rich text" rendering, with 3 levels: Styling, Formatting / Layout, and Rendering are each handled separately as three different levels in the stack -- simplifies many things to separate in this way, and makes the final render pass maximally efficient and high-performance, at the potential cost of some memory redundancy.

    • RuneRender -- struct that has everything needed to render a single rune (font face, colors, decorations, position, size, rotation, scale)
    • SpanRender -- contains one span of runes (e.g., a line of text). It has methods for basic rune layout.
    • TextRender -- contains one or more spans -- e.g., a paragraph. It has methods for rendering, styling, layout, including parsing of HTML-style rich text tags, including links (see TextLink).
    • In the Node2D passes, styling happens in Style2D (obviously), layout first pass happens in Size2D, which generates size info for the fully formatted text. Layout2D may trigger a re-layout if word wrapping takes place. Render2D then has a fully-compiled blob of RuneRender's to very quickly splat out with no further ado..
  • The SVG and most 2D default coordinate systems have 0,0 at the upper-left. The default 3D coordinate system flips the Y axis so 0,0 is at the lower left effectively (actually it uses center-based coordinates so 0,0 is in the center of the image, effectively -- everything is defined by the camera anyway)

  • Basic CSS styling is based on the Box model: https://www.w3schools.com/css/css_boxmodel.asp -- see also the Box shadow model https://www.w3schools.com/css/css3_shadows.asp -- general html spec: https://www.w3.org/TR/html5/index.html#contents -- better ref section of w3schools for css spec: https://www.w3schools.com/cssref/default.asp

  • See Naming for naming conventions (important!).

  • Every non-terminal Widget must either be a Layout or take full responsibility for everything under it -- i.e., all arbitrary collections of widgets must be Layouts -- only the layout has all the logic necessary for organizing the geometry of its children. There is only one Layout type that supports all forms of Layout -- and it is a proper Widget -- not a side class like in Qt Widgets. The Frame is a type of Layout that draws a frame around itself.

  • General Widget method conventions:

    • SetValue kinds of methods are wrapped in updates, but do NOT emit a signal
    • SetValueAction calls SetValue and emits the signal
    • this allows other users of the widget that also recv the signal to not trigger themselves, but typically you want the update, so it makes sense to have that in the basic version. ValueView in particular requires this kind of behavior. todo: go back and make this more consistent.

Five Rendering Passes

The Five Steps of rendering, defined in the Node2D interface API, are:

  1. Init2D: In a MeFirst downward pass, Viewport pointer is set, styles are initialized, and any other widget-specific init is done.

  2. Style2D: In a MeFirst downward pass, all properties are cached out in an inherited manner, and incorporating any css styles, into either the Paint (SVG) or Style (Widget) object for each Node. Only done once after structural changes -- styles are not for dynamic changes.

  3. Size2D: DepthFirst downward pass, each node first calls g.Layout.Reset(), then sets their LayoutSize according to their own intrinsic size parameters, and/or those of its children if it is a Layout.

  4. Layout2D: MeFirst downward pass (each node calls on its children at appropriate point) with relevant parent BBox that the children are constrained to render within -- they then intersect this BBox with their own BBox (from BBox2D) -- typically just call Layout2DBase for default behavior -- and add parent position to AllocPos. Layout does all its sizing and positioning of children in this pass, based on the Size2D data gathered bottom-up and constraints applied top-down from higher levels. Typically only a single iteration is required but multiple are supported (needed for word-wrapped text or flow layouts).

  5. Render2D: Final rendering pass, each node is fully responsible for rendering its own children, to provide maximum flexibility (see Render2DChildren) -- bracket the render calls in PushBounds / PopBounds and a false from PushBounds indicates that VpBBox is empty and no rendering should occur. Nodes typically connect / disconnect to receive events from the window based on this visibility here (using ConnectEvents2D method, which can be overridden to customize behavior).

Some additional non-standard cases:

  • Move2D: optional pass invoked by scrollbars to move elements relative to their previously-assigned positions.

  • SVG nodes skip the Size and Layout passes, and render directly into parent SVG viewport

  • Anyone wanting to add new nodes etc should definitely be familiar with gi/node2d.go and as usual, choose the closest existing case and just copy / paste / modify until it works :)

  • See giv.TextView for a highly interactive rendering dynamic -- the above passes provide an initial setup, but then lots of on-demand rendering happens beyond that scope. In addition, it interacts extensively with its parent Layout as it changes size and for scrolling etc.

Clone this wiki locally