shithub: mycel

Download patch

ref: 312c959f10a0d5394aa2f56af489fc144b6c8862
parent: 8c5bc6a4fb5d3c393831625ba538c3b3cfe2dd01
author: Philip Silva <philip.silva@protonmail.com>
date: Mon Jun 28 19:40:52 EDT 2021

label.go from duit with line height 1.2

Move modified duit code into separate package

diff: cannot open b/browser/duitx//null: file does not exist: 'b/browser/duitx//null'
--- a/browser/box.go
+++ /dev/null
@@ -1,220 +1,0 @@
-package browser
-
-// Original code from github.com/mjl-/duit
-//
-// Copyright 2018 Mechiel Lukkien mechiel@ueber.net
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy of this
-// software and associated documentation files (the "Software"), to deal in the Software
-// without restriction, including without limitation the rights to use, copy, modify, merge,
-// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
-// to whom the Software is furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in all copies or
-// substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
-// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
-// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
-// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-import (
-	"image"
-
-	"9fans.net/go/draw"
-	"github.com/mjl-/duit"
-)
-
-// NewBox returns a box containing all uis in its Kids field.
-func NewBox(uis ...duit.UI) *Box {
-	kids := make([]*duit.Kid, len(uis))
-	for i, ui := range uis {
-		kids[i] = &duit.Kid{UI: ui}
-	}
-	return &Box{Kids: kids}
-}
-
-// NewReverseBox returns a box containing all uis in original order in its Kids field, with the Reverse field set.
-func NewReverseBox(uis ...duit.UI) *Box {
-	kids := make([]*duit.Kid, len(uis))
-	for i, ui := range uis {
-		kids[i] = &duit.Kid{UI: ui}
-	}
-	return &Box{Kids: kids, Reverse: true}
-}
-
-// Box keeps elements on a line as long as they fit, then moves on to the next line.
-type Box struct {
-	Kids       []*duit.Kid      // Kids and UIs in this box.
-	Reverse    bool        // Lay out children from bottom to top. First kid will be at the bottom.
-	Margin     duit.Space // In lowDPI pixels, will be adjusted for highDPI screens.
-	Padding    duit.Space       // Padding inside box, so children don't touch the sides; in lowDPI pixels, also adjusted for highDPI screens.
-	Valign     duit.Valign      // How to align children on a line.
-	Width      int         // 0 means dynamic (as much as needed), -1 means full width, >0 means that exact amount of lowDPI pixels.
-	Height     int         // 0 means dynamic (as much as needed), -1 means full height, >0 means that exact amount of lowDPI pixels.
-	MaxWidth   int         // if >0, the max number of lowDPI pixels that will be used.
-	ContentBox bool        // Use ContentBox (BorderBox by default)
-	Background *draw.Image `json:"-"` // Background for this box, instead of default duit background.
-
-	size image.Point // of entire box, including padding but excluding margin
-}
-
-var _ duit.UI = &Box{}
-
-func (ui *Box) Layout(dui *duit.DUI, self *duit.Kid, sizeAvail image.Point, force bool) {
-	debugLayout(dui, self)
-	if duit.KidsLayout(dui, self, ui.Kids, force) {
-		return
-	}
-
-	if ui.Width < 0 && ui.MaxWidth > 0 {
-		panic("combination ui.Width < 0 and ui.MaxWidth > 0 invalid")
-	}
-
-	padding := dui.ScaleSpace(ui.Padding)
-	margin := dui.ScaleSpace(ui.Margin)
-
-	// widths and heights
-	bbw := dui.Scale(ui.Width)
-	bbmaxw := dui.Scale(ui.MaxWidth)
-	bbh := dui.Scale(ui.Height)
-
-	if ui.ContentBox {
-		bbw += margin.Dx()+padding.Dx()
-		bbmaxw += margin.Dx()+padding.Dx()
-		bbh += margin.Dy()+padding.Dy()
-	}
-
-	osize := sizeAvail
-	if ui.Width > 0 && bbw < sizeAvail.X {
-		sizeAvail.X = bbw
-	} else if ui.MaxWidth > 0 && bbmaxw < sizeAvail.X {
-		// note: ui.Width is currently the same as MaxWidth, but that might change when we don't mind extending beyong given X, eg with horizontal scroll
-		sizeAvail.X = bbmaxw
-	}
-	if ui.Height > 0 {
-		sizeAvail.Y = bbh
-	}
-	sizeAvail = sizeAvail.Sub(padding.Size()).Sub(margin.Size())
-	nx := 0 // number on current line
-
-	// variables below are about box contents excluding offsets for padding and margin
-	cur := image.ZP
-	xmax := 0  // max x seen so far
-	lineY := 0 // max y of current line
-
-	fixValign := func(kids []*duit.Kid) {
-		if len(kids) < 2 {
-			return
-		}
-		for _, k := range kids {
-			switch ui.Valign {
-			case duit.ValignTop:
-			case duit.ValignMiddle:
-				k.R = k.R.Add(image.Pt(0, (lineY-k.R.Dy())/2))
-			case duit.ValignBottom:
-				k.R = k.R.Add(image.Pt(0, lineY-k.R.Dy()))
-			}
-		}
-	}
-
-	for i, k := range ui.Kids {
-		k.UI.Layout(dui, k, sizeAvail.Sub(image.Pt(0, cur.Y+lineY)), true)
-		childSize := k.R.Size()
-		var kr image.Rectangle
-		if nx == 0 || cur.X+childSize.X <= sizeAvail.X {
-			kr = rect(childSize).Add(cur).Add(padding.Topleft())
-			cur.X += childSize.X
-			lineY = maximum(lineY, childSize.Y)
-			nx += 1
-		} else {
-			if nx > 0 {
-				fixValign(ui.Kids[i-nx : i])
-				cur.X = 0
-				cur.Y += lineY + margin.Topleft().Y
-			}
-			// Add padding translation, so the child UI can be drawn right there
-			kr = rect(childSize).Add(cur).Add(padding.Topleft())
-			nx = 1
-			cur.X = childSize.X
-			lineY = childSize.Y
-		}
-		k.R = kr
-		if xmax < cur.X {
-			xmax = cur.X
-		}
-	}
-	fixValign(ui.Kids[len(ui.Kids)-nx : len(ui.Kids)])
-	cur.Y += lineY
-
-	if ui.Reverse {
-		bottomY := cur.Y + padding.Dy()
-		for _, k := range ui.Kids {
-			y1 := bottomY - k.R.Min.Y
-			y0 := y1 - k.R.Dy()
-			k.R = image.Rect(k.R.Min.X, y0, k.R.Max.X, y1)
-		}
-	}
-
-	ui.size = image.Pt(xmax, cur.Y).Add(padding.Size())
-	if ui.Width < 0 {
-		ui.size.X = osize.X
-	}
-	if ui.Height < 0 && ui.size.Y < osize.Y {
-		ui.size.Y = osize.Y
-	}
-	self.R = rect(ui.size.Add(margin.Size()))
-}
-
-func (ui *Box) Draw(dui *duit.DUI, self *duit.Kid, img *draw.Image, orig image.Point, m draw.Mouse, force bool) {
-	margin := dui.ScaleSpace(ui.Margin)
-	orig = orig.Add(margin.Topleft())
-	duit.KidsDraw(dui, self, ui.Kids, ui.size, ui.Background, img, orig, m, force)
-}
-
-func (ui *Box) Mouse(dui *duit.DUI, self *duit.Kid, m draw.Mouse, origM draw.Mouse, orig image.Point) (r duit.Result) {
-	margin := dui.ScaleSpace(ui.Margin)
-	origM.Point = origM.Point.Sub(margin.Topleft())
-	m.Point = m.Point.Sub(margin.Topleft())
-	return duit.KidsMouse(dui, self, ui.Kids, m, origM, orig)
-}
-
-func (ui *Box) Key(dui *duit.DUI, self *duit.Kid, k rune, m draw.Mouse, orig image.Point) (r duit.Result) {
-	// nil check for tests
-	if dui != nil {
-		margin := dui.ScaleSpace(ui.Margin)
-		m.Point = m.Point.Sub(margin.Topleft())
-	}
-	return duit.KidsKey(dui, self, ui.orderedKids(), k, m, orig)
-}
-
-func (ui *Box) orderedKids() []*duit.Kid {
-	if !ui.Reverse {
-		return ui.Kids
-	}
-	n := len(ui.Kids)
-	kids := make([]*duit.Kid, n)
-	for i := range ui.Kids {
-		kids[i] = ui.Kids[n-1-i]
-	}
-	return kids
-}
-
-func (ui *Box) FirstFocus(dui *duit.DUI, self *duit.Kid) *image.Point {
-	return duit.KidsFirstFocus(dui, self, ui.orderedKids())
-}
-
-func (ui *Box) Focus(dui *duit.DUI, self *duit.Kid, o duit.UI) *image.Point {
-	return duit.KidsFocus(dui, self, ui.Kids, o)
-}
-
-func (ui *Box) Mark(self *duit.Kid, o duit.UI, forLayout bool) (marked bool) {
-	return duit.KidsMark(self, ui.Kids, o, forLayout)
-}
-
-func (ui *Box) Print(self *duit.Kid, indent int) {
-	duit.PrintUI("Box", self, indent)
-	duit.KidsPrint(ui.Kids, indent+1)
-}
--- a/browser/browser.go
+++ b/browser/browser.go
@@ -14,6 +14,7 @@
 	"net/url"
 	"github.com/psilva261/opossum"
 	"github.com/psilva261/opossum/browser/cache"
+	"github.com/psilva261/opossum/browser/duitx"
 	"github.com/psilva261/opossum/browser/fs"
 	"github.com/psilva261/opossum/browser/history"
 	"github.com/psilva261/opossum/img"
@@ -63,7 +64,7 @@
 var colorCache = make(map[draw.Color]*draw.Image)
 var imageCache = make(map[string]*draw.Image)
 var log *logger.Logger
-var scroller *Scroll
+var scroller *duitx.Scroll
 var display *draw.Display
 
 func SetLogger(l *logger.Logger) {
@@ -71,7 +72,7 @@
 }
 
 type Label struct {
-	*duit.Label
+	*duitx.Label
 
 	n *nodes.Node
 }
@@ -78,7 +79,7 @@
 
 func NewLabel(t string, n *nodes.Node) *Label {
 	return &Label{
-		Label: &duit.Label{
+		Label: &duitx.Label{
 			Text: t + " ",
 			Font: n.Font(),
 		},
@@ -141,7 +142,7 @@
 	}
 	lines := len(strings.Split(s, "\n"))
 	edit.Append([]byte(s))
-	cv.UI = &Box{
+	cv.UI = &duitx.Box{
 		Kids:   duit.NewKids(edit),
 		Height: int(n.FontHeight()) * (lines+2),
 	}
@@ -326,7 +327,7 @@
 	}
 }
 
-func newBoxElement(ui duit.UI, n *nodes.Node) (box *Box, ok bool) {
+func newBoxElement(ui duit.UI, n *nodes.Node) (box *duitx.Box, ok bool) {
 	if ui == nil {
 		return nil, false
 	}
@@ -379,7 +380,7 @@
 		return nil, false
 	}
 
-	box = &Box{
+	box = &duitx.Box{
 		Kids:       duit.NewKids(ui),
 		Width:      w,
 		Height:     h,
@@ -405,7 +406,7 @@
 	// }
 	//
 	// Make boxes use full size for image backgrounds
-	box, ok := el.UI.(*Box)
+	box, ok := el.UI.(*duitx.Box)
 	if ok && box.Width > 0 && box.Height > 0 {
 		uiSize := image.Point{X: box.Width, Y: box.Height}
 		duit.KidsDraw(dui, self, box.Kids, uiSize, box.Background, img, orig, m, force)
@@ -426,7 +427,7 @@
 	}
 
 	// Make boxes use full size for image backgrounds
-	box, ok := el.UI.(*Box)
+	box, ok := el.UI.(*duitx.Box)
 	if ok && box.Width > 0 && box.Height > 0 {
 		//dui.debugLayout(self)
 		//if ui.Image == nil {
@@ -563,7 +564,7 @@
 	if n.Css("height") == "" {
 		n.SetCss("height", fmt.Sprintf("%vpx", 4 * n.Font().Height))
 	}
-	return NewElement(NewScroll(l), n)
+	return NewElement(duitx.NewScroll(l), n)
 }
 
 func NewTextArea(n *nodes.Node) *Element {
@@ -584,7 +585,7 @@
 
 	el := NewElement(edit, n)
 	el.Changed = func(e *Element) {
-		ed := e.UI.(*Box).Kids[0].UI.(*duit.Edit)
+		ed := e.UI.(*duitx.Box).Kids[0].UI.(*duit.Edit)
 
 		tt, err := ed.Text()
 		if err != nil {
@@ -851,11 +852,11 @@
 			}
 		}
 
-		return &Box{
+		return &duitx.Box{
 			Kids:    duit.NewKids(finalUis...),
 		}
 	} else {
-		return &Grid{
+		return &duitx.Grid{
 			Columns: len(es),
 			Padding: duit.NSpace(len(es), duit.SpaceXY(0, 3)),
 			Halign:  halign,
@@ -877,7 +878,7 @@
 		uis = append(uis, e)
 	}
 
-	return &Grid{
+	return &duitx.Grid{
 		Columns: 1,
 		Padding: duit.NSpace(1, duit.SpaceXY(0, 3)),
 		Halign:  []duit.Halign{duit.HalignLeft},
@@ -978,7 +979,7 @@
 		}
 
 		return NewElement(
-			&Grid{
+			&duitx.Grid{
 				Columns: numCols,
 				Padding: duit.NSpace(numCols, duit.SpaceXY(0, 3)),
 				Halign:  halign,
@@ -1203,9 +1204,9 @@
 	switch v := ui.(type) {
 	case nil:
 		panic("null")
-	case *Scroll:
+	case *duitx.Scroll:
 		traverseTree(r+1, v.Kid.UI, f)
-	case *Box:
+	case *duitx.Box:
 		for _, kid := range v.Kids {
 			traverseTree(r+1, kid.UI, f)
 		}
@@ -1216,12 +1217,12 @@
 			return
 		}
 		traverseTree(r+1, v.UI, f)
-	case *Grid:
+	case *duitx.Grid:
 		for _, kid := range v.Kids {
 			traverseTree(r+1, kid.UI, f)
 		}
 	case *duit.Image:
-	case *duit.Label:
+	case *duitx.Label:
 	case *Label:
 		traverseTree(r+1, v.Label, f)
 	case *Image:
@@ -1258,7 +1259,7 @@
 	case *duit.Scroll:
 		fmt.Printf("duit.Scroll\n")
 		printTree(r+1, v.Kid.UI)
-	case *Box:
+	case *duitx.Box:
 		fmt.Printf("Box\n")
 		for _, kid := range v.Kids {
 			printTree(r+1, kid.UI)
@@ -1270,7 +1271,7 @@
 		}
 		fmt.Printf("Element\n")
 		printTree(r+1, v.UI)
-	case *Grid:
+	case *duitx.Grid:
 		fmt.Printf("Grid %vx%v\n", len(v.Kids)/v.Columns, v.Columns)
 		for _, kid := range v.Kids {
 			printTree(r+1, kid.UI)
--- a/browser/browser_test.go
+++ b/browser/browser_test.go
@@ -9,6 +9,7 @@
 	"net/http"
 	"net/url"
 	"github.com/chris-ramon/douceur/css"
+	"github.com/psilva261/opossum/browser/duitx"
 	"github.com/psilva261/opossum/logger"
 	"github.com/psilva261/opossum/nodes"
 	"github.com/psilva261/opossum/style"
@@ -90,12 +91,12 @@
 			}
 		}
 		if d == "inline" {
-			b := v.UI.(*Box)
+			b := v.UI.(*duitx.Box)
 			if len(b.Kids) != 3 {
 				t.Fatalf("%+v", b)
 			}
 		} else {
-			if g := v.UI.(*Grid); g.Columns != 1 || len(g.Kids) != 3 {
+			if g := v.UI.(*duitx.Grid); g.Columns != 1 || len(g.Kids) != 3 {
 				t.Fatalf("%+v", g)
 			}
 		}
@@ -222,7 +223,7 @@
 			break
 		}
 	}
-	el := e.UI.(*Box)
+	el := e.UI.(*duitx.Box)
 	return el.Kids, true
 }
 
--- /dev/null
+++ b/browser/duitx/box.go
@@ -1,0 +1,220 @@
+package duitx
+
+// Original code from github.com/mjl-/duit
+//
+// Copyright 2018 Mechiel Lukkien mechiel@ueber.net
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this
+// software and associated documentation files (the "Software"), to deal in the Software
+// without restriction, including without limitation the rights to use, copy, modify, merge,
+// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
+// to whom the Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all copies or
+// substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import (
+	"image"
+
+	"9fans.net/go/draw"
+	"github.com/mjl-/duit"
+)
+
+// NewBox returns a box containing all uis in its Kids field.
+func NewBox(uis ...duit.UI) *Box {
+	kids := make([]*duit.Kid, len(uis))
+	for i, ui := range uis {
+		kids[i] = &duit.Kid{UI: ui}
+	}
+	return &Box{Kids: kids}
+}
+
+// NewReverseBox returns a box containing all uis in original order in its Kids field, with the Reverse field set.
+func NewReverseBox(uis ...duit.UI) *Box {
+	kids := make([]*duit.Kid, len(uis))
+	for i, ui := range uis {
+		kids[i] = &duit.Kid{UI: ui}
+	}
+	return &Box{Kids: kids, Reverse: true}
+}
+
+// Box keeps elements on a line as long as they fit, then moves on to the next line.
+type Box struct {
+	Kids       []*duit.Kid      // Kids and UIs in this box.
+	Reverse    bool        // Lay out children from bottom to top. First kid will be at the bottom.
+	Margin     duit.Space // In lowDPI pixels, will be adjusted for highDPI screens.
+	Padding    duit.Space       // Padding inside box, so children don't touch the sides; in lowDPI pixels, also adjusted for highDPI screens.
+	Valign     duit.Valign      // How to align children on a line.
+	Width      int         // 0 means dynamic (as much as needed), -1 means full width, >0 means that exact amount of lowDPI pixels.
+	Height     int         // 0 means dynamic (as much as needed), -1 means full height, >0 means that exact amount of lowDPI pixels.
+	MaxWidth   int         // if >0, the max number of lowDPI pixels that will be used.
+	ContentBox bool        // Use ContentBox (BorderBox by default)
+	Background *draw.Image `json:"-"` // Background for this box, instead of default duit background.
+
+	size image.Point // of entire box, including padding but excluding margin
+}
+
+var _ duit.UI = &Box{}
+
+func (ui *Box) Layout(dui *duit.DUI, self *duit.Kid, sizeAvail image.Point, force bool) {
+	debugLayout(dui, self)
+	if duit.KidsLayout(dui, self, ui.Kids, force) {
+		return
+	}
+
+	if ui.Width < 0 && ui.MaxWidth > 0 {
+		panic("combination ui.Width < 0 and ui.MaxWidth > 0 invalid")
+	}
+
+	padding := dui.ScaleSpace(ui.Padding)
+	margin := dui.ScaleSpace(ui.Margin)
+
+	// widths and heights
+	bbw := dui.Scale(ui.Width)
+	bbmaxw := dui.Scale(ui.MaxWidth)
+	bbh := dui.Scale(ui.Height)
+
+	if ui.ContentBox {
+		bbw += margin.Dx()+padding.Dx()
+		bbmaxw += margin.Dx()+padding.Dx()
+		bbh += margin.Dy()+padding.Dy()
+	}
+
+	osize := sizeAvail
+	if ui.Width > 0 && bbw < sizeAvail.X {
+		sizeAvail.X = bbw
+	} else if ui.MaxWidth > 0 && bbmaxw < sizeAvail.X {
+		// note: ui.Width is currently the same as MaxWidth, but that might change when we don't mind extending beyong given X, eg with horizontal scroll
+		sizeAvail.X = bbmaxw
+	}
+	if ui.Height > 0 {
+		sizeAvail.Y = bbh
+	}
+	sizeAvail = sizeAvail.Sub(padding.Size()).Sub(margin.Size())
+	nx := 0 // number on current line
+
+	// variables below are about box contents excluding offsets for padding and margin
+	cur := image.ZP
+	xmax := 0  // max x seen so far
+	lineY := 0 // max y of current line
+
+	fixValign := func(kids []*duit.Kid) {
+		if len(kids) < 2 {
+			return
+		}
+		for _, k := range kids {
+			switch ui.Valign {
+			case duit.ValignTop:
+			case duit.ValignMiddle:
+				k.R = k.R.Add(image.Pt(0, (lineY-k.R.Dy())/2))
+			case duit.ValignBottom:
+				k.R = k.R.Add(image.Pt(0, lineY-k.R.Dy()))
+			}
+		}
+	}
+
+	for i, k := range ui.Kids {
+		k.UI.Layout(dui, k, sizeAvail.Sub(image.Pt(0, cur.Y+lineY)), true)
+		childSize := k.R.Size()
+		var kr image.Rectangle
+		if nx == 0 || cur.X+childSize.X <= sizeAvail.X {
+			kr = rect(childSize).Add(cur).Add(padding.Topleft())
+			cur.X += childSize.X
+			lineY = maximum(lineY, childSize.Y)
+			nx += 1
+		} else {
+			if nx > 0 {
+				fixValign(ui.Kids[i-nx : i])
+				cur.X = 0
+				cur.Y += lineY + margin.Topleft().Y
+			}
+			// Add padding translation, so the child UI can be drawn right there
+			kr = rect(childSize).Add(cur).Add(padding.Topleft())
+			nx = 1
+			cur.X = childSize.X
+			lineY = childSize.Y
+		}
+		k.R = kr
+		if xmax < cur.X {
+			xmax = cur.X
+		}
+	}
+	fixValign(ui.Kids[len(ui.Kids)-nx : len(ui.Kids)])
+	cur.Y += lineY
+
+	if ui.Reverse {
+		bottomY := cur.Y + padding.Dy()
+		for _, k := range ui.Kids {
+			y1 := bottomY - k.R.Min.Y
+			y0 := y1 - k.R.Dy()
+			k.R = image.Rect(k.R.Min.X, y0, k.R.Max.X, y1)
+		}
+	}
+
+	ui.size = image.Pt(xmax, cur.Y).Add(padding.Size())
+	if ui.Width < 0 {
+		ui.size.X = osize.X
+	}
+	if ui.Height < 0 && ui.size.Y < osize.Y {
+		ui.size.Y = osize.Y
+	}
+	self.R = rect(ui.size.Add(margin.Size()))
+}
+
+func (ui *Box) Draw(dui *duit.DUI, self *duit.Kid, img *draw.Image, orig image.Point, m draw.Mouse, force bool) {
+	margin := dui.ScaleSpace(ui.Margin)
+	orig = orig.Add(margin.Topleft())
+	duit.KidsDraw(dui, self, ui.Kids, ui.size, ui.Background, img, orig, m, force)
+}
+
+func (ui *Box) Mouse(dui *duit.DUI, self *duit.Kid, m draw.Mouse, origM draw.Mouse, orig image.Point) (r duit.Result) {
+	margin := dui.ScaleSpace(ui.Margin)
+	origM.Point = origM.Point.Sub(margin.Topleft())
+	m.Point = m.Point.Sub(margin.Topleft())
+	return duit.KidsMouse(dui, self, ui.Kids, m, origM, orig)
+}
+
+func (ui *Box) Key(dui *duit.DUI, self *duit.Kid, k rune, m draw.Mouse, orig image.Point) (r duit.Result) {
+	// nil check for tests
+	if dui != nil {
+		margin := dui.ScaleSpace(ui.Margin)
+		m.Point = m.Point.Sub(margin.Topleft())
+	}
+	return duit.KidsKey(dui, self, ui.orderedKids(), k, m, orig)
+}
+
+func (ui *Box) orderedKids() []*duit.Kid {
+	if !ui.Reverse {
+		return ui.Kids
+	}
+	n := len(ui.Kids)
+	kids := make([]*duit.Kid, n)
+	for i := range ui.Kids {
+		kids[i] = ui.Kids[n-1-i]
+	}
+	return kids
+}
+
+func (ui *Box) FirstFocus(dui *duit.DUI, self *duit.Kid) *image.Point {
+	return duit.KidsFirstFocus(dui, self, ui.orderedKids())
+}
+
+func (ui *Box) Focus(dui *duit.DUI, self *duit.Kid, o duit.UI) *image.Point {
+	return duit.KidsFocus(dui, self, ui.Kids, o)
+}
+
+func (ui *Box) Mark(self *duit.Kid, o duit.UI, forLayout bool) (marked bool) {
+	return duit.KidsMark(self, ui.Kids, o, forLayout)
+}
+
+func (ui *Box) Print(self *duit.Kid, indent int) {
+	duit.PrintUI("Box", self, indent)
+	duit.KidsPrint(ui.Kids, indent+1)
+}
--- /dev/null
+++ b/browser/duitx/duitx.go
@@ -1,0 +1,11 @@
+package duitx
+
+import (
+	"github.com/psilva261/opossum/logger"
+)
+
+var log *logger.Logger
+
+func SetLogger(l *logger.Logger) {
+	log = l
+}
\ No newline at end of file
--- /dev/null
+++ b/browser/duitx/grid.go
@@ -1,0 +1,206 @@
+package duitx
+
+// Original code from github.com/mjl-/duit
+//
+// Copyright 2018 Mechiel Lukkien mechiel@ueber.net
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this
+// software and associated documentation files (the "Software"), to deal in the Software
+// without restriction, including without limitation the rights to use, copy, modify, merge,
+// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
+// to whom the Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all copies or
+// substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import (
+	"fmt"
+	"image"
+
+	"9fans.net/go/draw"
+	"github.com/mjl-/duit"
+)
+
+// Grid lays out other duit.UIs in a table-like grid.
+type Grid struct {
+	Kids       []*duit.Kid      // Holds UIs in the grid, per row.
+	Columns    int         // Number of clumns.
+	Valign     []duit.Valign    // Vertical alignment per column.
+	Halign     []duit.Halign    // Horizontal alignment per column.
+	Padding    []duit.Space     // Padding in lowDPI pixels per column.
+	Width      int         // -1 means full width, 0 means automatic width, >0 means exactly that many lowDPI pixels.
+	Background *draw.Image `json:"-"` // Background color.
+
+	widths  []int
+	heights []int
+	size    image.Point
+}
+
+var _ duit.UI = &Grid{}
+
+func (ui *Grid) Layout(dui *duit.DUI, self *duit.Kid, sizeAvail image.Point, force bool) {
+	debugLayout(dui, self)
+	if duit.KidsLayout(dui, self, ui.Kids, force) {
+		return
+	}
+
+	if ui.Valign != nil && len(ui.Valign) != ui.Columns {
+		panic(fmt.Sprintf("len(valign) = %d, should be ui.Columns = %d", len(ui.Valign), ui.Columns))
+	}
+	if ui.Halign != nil && len(ui.Halign) != ui.Columns {
+		panic(fmt.Sprintf("len(halign) = %d, should be ui.Columns = %d", len(ui.Halign), ui.Columns))
+	}
+	if ui.Padding != nil && len(ui.Padding) != ui.Columns {
+		panic(fmt.Sprintf("len(padding) = %d, should be ui.Columns = %d", len(ui.Padding), ui.Columns))
+	}
+	if len(ui.Kids)%ui.Columns != 0 {
+		panic(fmt.Sprintf("len(kids) = %d, should be multiple of ui.Columns = %d", len(ui.Kids), ui.Columns))
+	}
+
+	scaledWidth := dui.Scale(ui.Width)
+	if scaledWidth > 0 && scaledWidth < sizeAvail.X {
+		ui.size.X = scaledWidth
+	}
+
+	ui.widths = make([]int, ui.Columns) // widths include padding
+	spaces := make([]duit.Space, ui.Columns)
+	if ui.Padding != nil {
+		for i, pad := range ui.Padding {
+			spaces[i] = dui.ScaleSpace(pad)
+		}
+	}
+	width := 0                       // total width so far
+	x := make([]int, len(ui.widths)) // x offsets per column
+	x[0] = 0
+
+	// first determine the column widths
+	for col := 0; col < ui.Columns; col++ {
+		if col > 0 {
+			x[col] = x[col-1] + ui.widths[col-1]
+		}
+		ui.widths[col] = 0
+		newDx := 0
+		space := spaces[col]
+		for i := col; i < len(ui.Kids); i += ui.Columns {
+			k := ui.Kids[i]
+			k.UI.Layout(dui, k, image.Pt(sizeAvail.X-width-space.Dx(), sizeAvail.Y-space.Dy()), true)
+			newDx = maximum(newDx, k.R.Dx()+space.Dx())
+		}
+		ui.widths[col] = newDx
+		width += ui.widths[col]
+	}
+	if scaledWidth < 0 && width < sizeAvail.X {
+		leftover := sizeAvail.X - width
+		given := 0
+		for i := range ui.widths {
+			x[i] += given
+			var dx int
+			if i == len(ui.widths)-1 {
+				dx = leftover - given
+			} else {
+				dx = leftover / len(ui.widths)
+			}
+			ui.widths[i] += dx
+			given += dx
+		}
+	}
+
+	// now determine row heights
+	ui.heights = make([]int, (len(ui.Kids)+ui.Columns-1)/ui.Columns)
+	height := 0                       // total height so far
+	y := make([]int, len(ui.heights)) // including padding
+	y[0] = 0
+	for i := 0; i < len(ui.Kids); i += ui.Columns {
+		row := i / ui.Columns
+		if row > 0 {
+			y[row] = y[row-1] + ui.heights[row-1]
+		}
+		rowDy := 0
+		for col := 0; col < ui.Columns; col++ {
+			space := spaces[col]
+			k := ui.Kids[i+col]
+			k.UI.Layout(dui, k, image.Pt(ui.widths[col]-space.Dx(), sizeAvail.Y-y[row]-space.Dy()), true)
+			offset := image.Pt(x[col], y[row]).Add(space.Topleft())
+			k.R = k.R.Add(offset) // aligned in top left, fixed for halign/valign later on
+			rowDy = maximum(rowDy, k.R.Dy()+space.Dy())
+		}
+		ui.heights[row] = rowDy
+		height += ui.heights[row]
+	}
+
+	// now shift the kids for right valign/halign
+	for i, k := range ui.Kids {
+		row := i / ui.Columns
+		col := i % ui.Columns
+		space := spaces[col]
+
+		valign := duit.ValignTop
+		halign := duit.HalignLeft
+		if ui.Valign != nil {
+			valign = ui.Valign[col]
+		}
+		if ui.Halign != nil {
+			halign = ui.Halign[col]
+		}
+		cellSize := image.Pt(ui.widths[col], ui.heights[row]).Sub(space.Size())
+		spaceX := 0
+		switch halign {
+		case duit.HalignLeft:
+		case duit.HalignMiddle:
+			spaceX = (cellSize.X - k.R.Dx()) / 2
+		case duit.HalignRight:
+			spaceX = cellSize.X - k.R.Dx()
+		}
+		spaceY := 0
+		switch valign {
+		case duit.ValignTop:
+		case duit.ValignMiddle:
+			spaceY = (cellSize.Y - k.R.Dy()) / 2
+		case duit.ValignBottom:
+			spaceY = cellSize.Y - k.R.Dy()
+		}
+		k.R = k.R.Add(image.Pt(spaceX, spaceY))
+	}
+
+	ui.size = image.Pt(width, height)
+	if ui.Width < 0 && ui.size.X < sizeAvail.X {
+		ui.size.X = sizeAvail.X
+	}
+	self.R = rect(ui.size)
+}
+
+func (ui *Grid) Draw(dui *duit.DUI, self *duit.Kid, img *draw.Image, orig image.Point, m draw.Mouse, force bool) {
+	duit.KidsDraw(dui, self, ui.Kids, ui.size, ui.Background, img, orig, m, force)
+}
+
+func (ui *Grid) Mouse(dui *duit.DUI, self *duit.Kid, m draw.Mouse, origM draw.Mouse, orig image.Point) (r duit.Result) {
+	return duit.KidsMouse(dui, self, ui.Kids, m, origM, orig)
+}
+
+func (ui *Grid) Key(dui *duit.DUI, self *duit.Kid, k rune, m draw.Mouse, orig image.Point) (r duit.Result) {
+	return duit.KidsKey(dui, self, ui.Kids, k, m, orig)
+}
+
+func (ui *Grid) FirstFocus(dui *duit.DUI, self *duit.Kid) *image.Point {
+	return duit.KidsFirstFocus(dui, self, ui.Kids)
+}
+
+func (ui *Grid) Focus(dui *duit.DUI, self *duit.Kid, o duit.UI) *image.Point {
+	return duit.KidsFocus(dui, self, ui.Kids, o)
+}
+
+func (ui *Grid) Mark(self *duit.Kid, o duit.UI, forLayout bool) (marked bool) {
+	return duit.KidsMark(self, ui.Kids, o, forLayout)
+}
+
+func (ui *Grid) Print(self *duit.Kid, indent int) {
+	duit.PrintUI(fmt.Sprintf("Grid columns=%d padding=%v", ui.Columns, ui.Padding), self, indent)
+	duit.KidsPrint(ui.Kids, indent+1)
+}
--- /dev/null
+++ b/browser/duitx/label.go
@@ -1,0 +1,146 @@
+package duitx
+
+// Original code from github.com/mjl-/duit
+//
+// Copyright 2018 Mechiel Lukkien mechiel@ueber.net
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this
+// software and associated documentation files (the "Software"), to deal in the Software
+// without restriction, including without limitation the rights to use, copy, modify, merge,
+// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
+// to whom the Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all copies or
+// substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import (
+	"image"
+
+	"9fans.net/go/draw"
+	"github.com/mjl-/duit"
+)
+
+// Label draws multiline text in a single font.:
+//
+// Keys:
+//	cmd-c, copy text
+//	\n, like button1 click, calls the Click function
+type Label struct {
+	Text  string           // Text to draw, wrapped at glyph boundary.
+	Font  *draw.Font       `json:"-"` // For drawing text.
+	Click func() (e duit.Event) `json:"-"` // Called on button1 click.
+
+	lines []string
+	size  image.Point
+	m     draw.Mouse
+}
+
+var _ duit.UI = &Label{}
+
+func (ui *Label) font(dui *duit.DUI) *draw.Font {
+	return dui.Font(ui.Font)
+}
+
+func (ui *Label) Layout(dui *duit.DUI, self *duit.Kid, sizeAvail image.Point, force bool) {
+	debugLayout(dui, self)
+
+	font := ui.font(dui)
+	ui.lines = []string{}
+	s := 0
+	x := 0
+	xmax := 0
+	for i, c := range ui.Text {
+		if c == '\n' {
+			xmax = maximum(xmax, x)
+			ui.lines = append(ui.lines, ui.Text[s:i])
+			s = i + 1
+			x = 0
+			continue
+		}
+		dx := font.StringWidth(string(c))
+		x += dx
+		if i-s == 0 || x <= sizeAvail.X {
+			continue
+		}
+		xmax = maximum(xmax, x-dx)
+		ui.lines = append(ui.lines, ui.Text[s:i])
+		s = i
+		x = dx
+	}
+	if s < len(ui.Text) || s == 0 {
+		ui.lines = append(ui.lines, ui.Text[s:])
+		xmax = maximum(xmax, x)
+	}
+	ui.size = image.Pt(xmax, len(ui.lines)*font.Height*12/10)
+	self.R = rect(ui.size)
+}
+
+func (ui *Label) Draw(dui *duit.DUI, self *duit.Kid, img *draw.Image, orig image.Point, m draw.Mouse, force bool) {
+	debugDraw(dui, self)
+
+	p := orig
+	font := ui.font(dui)
+	for _, line := range ui.lines {
+		img.String(p, dui.Regular.Normal.Text, image.ZP, font, line)
+		p.Y += font.Height*12/10
+	}
+}
+
+func (ui *Label) Mouse(dui *duit.DUI, self *duit.Kid, m draw.Mouse, origM draw.Mouse, orig image.Point) (r duit.Result) {
+	if m.In(rect(ui.size)) && ui.m.Buttons == 0 && m.Buttons == duit.Button1 && ui.Click != nil {
+		e := ui.Click()
+		propagateEvent(self, &r, e)
+	}
+	ui.m = m
+	return
+}
+
+func (ui *Label) Key(dui *duit.DUI, self *duit.Kid, k rune, m draw.Mouse, orig image.Point) (r duit.Result) {
+	switch k {
+	case '\n':
+		if ui.Click != nil {
+			e := ui.Click()
+			propagateEvent(self, &r, e)
+		}
+	case draw.KeyCmd + 'c':
+		dui.WriteSnarf([]byte(ui.Text))
+		r.Consumed = true
+	}
+	return
+}
+
+func (ui *Label) FirstFocus(dui *duit.DUI, self *duit.Kid) *image.Point {
+	return nil
+}
+
+func (ui *Label) Focus(dui *duit.DUI, self *duit.Kid, o duit.UI) *image.Point {
+	if ui != o {
+		return nil
+	}
+	return &image.ZP
+}
+
+func (ui *Label) Mark(self *duit.Kid, o duit.UI, forLayout bool) (marked bool) {
+	return self.Mark(o, forLayout)
+}
+
+func (ui *Label) Print(self *duit.Kid, indent int) {
+	duit.PrintUI("Label", self, indent)
+}
+
+func propagateEvent(self *duit.Kid, r *duit.Result, e duit.Event) {
+	if e.NeedLayout {
+		self.Layout = duit.Dirty
+	}
+	if e.NeedDraw {
+		self.Draw = duit.Dirty
+	}
+	r.Consumed = e.Consumed || r.Consumed
+}
--- /dev/null
+++ b/browser/duitx/scroll.go
@@ -1,0 +1,449 @@
+package duitx
+
+// Original code from github.com/mjl-/duit
+//
+// Copyright 2018 Mechiel Lukkien mechiel@ueber.net
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this
+// software and associated documentation files (the "Software"), to deal in the Software
+// without restriction, including without limitation the rights to use, copy, modify, merge,
+// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
+// to whom the Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all copies or
+// substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import (
+	"fmt"
+	"image"
+	"math"
+
+	"9fans.net/go/draw"
+	"github.com/mjl-/duit"
+)
+
+// Scroll shows a part of its single child, typically a box, and lets you scroll the content.
+type Scroll struct {
+	Kid    duit.Kid
+	Height int // < 0 means full height, 0 means as much as necessary, >0 means exactly that many lowdpi pixels
+
+	r             image.Rectangle // entire ui
+	barR          image.Rectangle
+	barActiveR    image.Rectangle
+	childR        image.Rectangle
+	Offset        int         // current scroll offset in pixels
+	img           *draw.Image // for child to draw on
+	scrollbarSize int
+	lastMouseUI   duit.UI
+	drawOffset int
+}
+
+var _ duit.UI = &Scroll{}
+
+// NewScroll returns a full-height scroll bar containing ui.
+func NewScroll(ui duit.UI) *Scroll {
+	return &Scroll{Height: -1, Kid: duit.Kid{UI: ui}}
+}
+
+func (ui *Scroll) Layout(dui *duit.DUI, self *duit.Kid, sizeAvail image.Point, force bool) {
+	debugLayout(dui, self)
+
+	if self.Layout == duit.Clean && !force {
+		return
+	}
+	self.Layout = duit.Clean
+	self.Draw = duit.Dirty
+	// todo: be smarter about DirtyKid
+
+	ui.scrollbarSize = dui.Scale(duit.ScrollbarSize)
+	scaledHeight := dui.Scale(ui.Height)
+	if scaledHeight > 0 && scaledHeight < sizeAvail.Y {
+		sizeAvail.Y = scaledHeight
+	}
+	ui.r = rect(sizeAvail)
+	ui.barR = ui.r
+	ui.barR.Max.X = ui.barR.Min.X + ui.scrollbarSize
+	ui.childR = ui.r
+	ui.childR.Min.X = ui.barR.Max.X
+
+	// todo: only force when sizeAvail or childR changed?
+	ui.Kid.UI.Layout(dui, &ui.Kid, image.Pt(ui.r.Dx()-ui.barR.Dx(), ui.r.Dy()), force)
+	ui.Kid.Layout = duit.Clean
+	ui.Kid.Draw = duit.Dirty
+
+	kY := ui.Kid.R.Dy()
+	if ui.r.Dy() > kY && ui.Height == 0 {
+		ui.barR.Max.Y = kY
+		ui.r.Max.Y = kY
+		ui.childR.Max.Y = kY
+	}
+	self.R = rect(ui.r.Size())
+}
+
+func (ui *Scroll) Draw(dui *duit.DUI, self *duit.Kid, img *draw.Image, orig image.Point, m draw.Mouse, force bool) {
+	debugDraw(dui, self)
+
+	if self.Draw == duit.Clean {
+		return
+	}
+	self.Draw = duit.Clean
+
+	if ui.r.Empty() {
+		return
+	}
+
+	// ui.scroll(0)
+	barHover := m.In(ui.barR)
+
+	bg := dui.ScrollBGNormal
+	vis := dui.ScrollVisibleNormal
+	if barHover {
+		bg = dui.ScrollBGHover
+		vis = dui.ScrollVisibleHover
+	}
+
+	h := ui.r.Dy()
+	uih := ui.Kid.R.Dy()
+	if uih > h {
+		barR := ui.barR.Add(orig)
+		img.Draw(barR, bg, nil, image.ZP)
+		barH := h * h / uih
+		barY := ui.Offset * h / uih
+		ui.barActiveR = ui.barR
+		ui.barActiveR.Min.Y += barY
+		ui.barActiveR.Max.Y = ui.barActiveR.Min.Y + barH
+		barActiveR := ui.barActiveR.Add(orig)
+		barActiveR.Max.X -= 1 // unscaled
+		img.Draw(barActiveR, vis, nil, image.ZP)
+	}
+
+	// draw child ui
+	if ui.childR.Empty() {
+		return
+	}
+	d := math.Abs(float64(ui.drawOffset - ui.Offset))
+	if  d > float64(ui.r.Max.Y) {
+		ui.Kid.Draw = duit.Dirty
+	}
+	if ui.img == nil || ui.drawRect().Size() != ui.img.R.Size() || ui.Kid.Draw == duit.Dirty {
+		var err error
+		if ui.img != nil {
+			ui.img.Free()
+			ui.img = nil
+		}
+		ui.Kid.Draw = duit.Dirty
+		if ui.Kid.R.Dx() == 0 || ui.Kid.R.Dy() == 0 {
+			return
+		}
+		ui.img, err = dui.Display.AllocImage(ui.drawRect(), draw.ARGB32, false, dui.BackgroundColor)
+		if duitError(dui, err, "allocimage") {
+			return
+		}
+		ui.drawOffset = ui.Offset
+	} else if ui.Kid.Draw == duit.Dirty {
+		ui.img.Draw(ui.img.R, dui.Background, nil, image.ZP)
+	}
+	m.Point = m.Point.Add(image.Pt(-ui.childR.Min.X, ui.Offset))
+	if ui.Kid.Draw != duit.Clean {
+		if force {
+			ui.Kid.Draw = duit.Dirty
+		}
+		ui.Kid.UI.Draw(dui, &ui.Kid, ui.img, image.ZP, m, ui.Kid.Draw == duit.Dirty)
+		ui.Kid.Draw = duit.Clean
+	}
+	img.Draw(ui.childR.Add(orig), ui.img, nil, image.Pt(0, ui.Offset))
+}
+
+// Allocate only an image buffer of view size ui.r
+// - which is translated by scroll offset ui.Offset - instead
+// of whole Kid view size ui.Kid.R which leads to much
+// faster render times for large pages. Add same size rectangles
+// above/below to decrease flickering.
+func (ui *Scroll) drawRect() image.Rectangle {
+	if 2*ui.r.Dy() > ui.Kid.R.Dy() {
+		return ui.Kid.R
+	}
+	r := image.Rectangle{
+		Min: ui.r.Min,
+		Max: image.Point{
+			ui.r.Max.X,
+			3*ui.r.Max.Y,
+		},
+	}
+	r = r.Add(image.Point{X:0, Y:ui.Offset-ui.r.Max.Y})
+	if r.Min.Y > ui.Offset {
+		r.Min.Y -= ui.Offset
+	}
+	return r
+}
+
+func (ui *Scroll) scroll(delta int) (changed bool) {
+	o := ui.Offset
+	ui.Offset += delta
+	ui.Offset = maximum(0, ui.Offset)
+	ui.Offset = minimum(ui.Offset, maximum(0, ui.Kid.R.Dy()-ui.childR.Dy()))
+	return o != ui.Offset
+}
+
+func (ui *Scroll) scrollKey(k rune) (consumed bool) {
+	switch k {
+	case draw.KeyUp:
+		return ui.scroll(-50)
+	case draw.KeyDown:
+		return ui.scroll(50)
+	case draw.KeyPageUp:
+		return ui.scroll(-200)
+	case draw.KeyPageDown:
+		return ui.scroll(200)
+	}
+	return false
+}
+
+func (ui *Scroll) scrollMouse(m draw.Mouse, scrollOnly bool) (consumed bool) {
+	switch m.Buttons {
+	case duit.Button4:
+		return ui.scroll(-m.Y / 4)
+	case duit.Button5:
+		return ui.scroll(m.Y / 4)
+	}
+
+	if scrollOnly {
+		return false
+	}
+	switch m.Buttons {
+	case duit.Button1:
+		return ui.scroll(-m.Y)
+	case duit.Button2:
+		Offset := m.Y * ui.Kid.R.Dy() / ui.barR.Dy()
+		OffsetMax := ui.Kid.R.Dy() - ui.childR.Dy()
+		Offset = maximum(0, minimum(Offset, OffsetMax))
+		o := ui.Offset
+		ui.Offset = Offset
+		return o != ui.Offset
+	case duit.Button3:
+		return ui.scroll(m.Y)
+	}
+	return false
+}
+
+func (ui *Scroll) result(dui *duit.DUI, self *duit.Kid, r *duit.Result, scrolled bool) {
+	if ui.Kid.Layout != duit.Clean {
+		ui.Kid.UI.Layout(dui, &ui.Kid, ui.childR.Size(), false)
+		ui.Kid.Layout = duit.Clean
+		ui.Kid.Draw = duit.Dirty
+		self.Draw = duit.Dirty
+	} else if ui.Kid.Draw != duit.Clean || scrolled {
+		self.Draw = duit.Dirty
+	}
+}
+
+func (ui *Scroll) Mouse(dui *duit.DUI, self *duit.Kid, m draw.Mouse, origM draw.Mouse, orig image.Point) (r duit.Result) {
+	nOrigM := origM
+	nOrigM.Point = nOrigM.Point.Add(image.Pt(-ui.scrollbarSize, ui.Offset))
+	nm := m
+	nm.Point = nm.Point.Add(image.Pt(-ui.scrollbarSize, ui.Offset))
+
+	if m.Buttons == 0 {
+		ui.Kid.UI.Mouse(dui, &ui.Kid, nm, nOrigM, image.ZP)
+		return
+	}
+	if m.Point.In(ui.barR) {
+		r.Hit = ui
+		r.Consumed = ui.scrollMouse(m, false)
+		self.Draw = duit.Dirty
+		return
+	} else if m.Point.In(ui.childR) {
+		r.Consumed = ui.scrollMouse(m, true)
+		if r.Consumed {
+			self.Draw = duit.Dirty
+			return
+		}
+		r = ui.Kid.UI.Mouse(dui, &ui.Kid, nm, nOrigM, image.ZP)
+		if r.Consumed {
+			self.Draw = duit.Dirty
+		}
+	}
+	return
+}
+
+func (ui *Scroll) Key(dui *duit.DUI, self *duit.Kid, k rune, m draw.Mouse, orig image.Point) (r duit.Result) {
+	if m.Point.In(ui.barR) {
+		r.Hit = ui
+		r.Consumed = ui.scrollKey(k)
+		if r.Consumed {
+			self.Draw = duit.Dirty
+		}
+	}
+	if m.Point.In(ui.childR) {
+		m.Point = m.Point.Add(image.Pt(-ui.scrollbarSize, ui.Offset))
+		r = ui.Kid.UI.Key(dui, &ui.Kid, k, m, image.ZP)
+		ui.warpScroll(dui, self, r.Warp, orig)
+		scrolled := false
+		if !r.Consumed {
+			scrolled = ui.scrollKey(k)
+			r.Consumed = scrolled
+		}
+		ui.result(dui, self, &r, scrolled)
+	}
+	return
+}
+
+func (ui *Scroll) warpScroll(dui *duit.DUI, self *duit.Kid, warp *image.Point, orig image.Point) {
+	if warp == nil {
+		return
+	}
+
+	Offset := ui.Offset
+	if warp.Y < ui.Offset {
+		ui.Offset = maximum(0, warp.Y-dui.Scale(40))
+	} else if warp.Y > ui.Offset+ui.r.Dy() {
+		ui.Offset = minimum(ui.Kid.R.Dy()-ui.r.Dy(), warp.Y+dui.Scale(40)-ui.r.Dy())
+	}
+	if Offset != ui.Offset {
+		if self != nil {
+			self.Draw = duit.Dirty
+		} else {
+			dui.MarkDraw(ui)
+		}
+	}
+	warp.Y -= ui.Offset
+	warp.X += orig.X + ui.scrollbarSize
+	warp.Y += orig.Y
+}
+
+func (ui *Scroll) _focus(dui *duit.DUI, p *image.Point) *image.Point {
+	if p == nil {
+		return nil
+	}
+	pp := p.Add(ui.childR.Min)
+	p = &pp
+	ui.warpScroll(dui, nil, p, image.ZP)
+	return p
+}
+
+func (ui *Scroll) FirstFocus(dui *duit.DUI, self *duit.Kid) *image.Point {
+	p := ui.Kid.UI.FirstFocus(dui, &ui.Kid)
+	return ui._focus(dui, p)
+}
+
+func (ui *Scroll) Focus(dui *duit.DUI, self *duit.Kid, o duit.UI) *image.Point {
+	if o == ui {
+		p := image.Pt(minimum(ui.scrollbarSize/2, ui.r.Dx()), minimum(ui.scrollbarSize/2, ui.r.Dy()))
+		return &p
+	}
+	p := ui.Kid.UI.Focus(dui, &ui.Kid, o)
+	return ui._focus(dui, p)
+}
+
+func (ui *Scroll) Mark(self *duit.Kid, o duit.UI, forLayout bool) (marked bool) {
+	if self.Mark(o, forLayout) {
+		return true
+	}
+	marked = ui.Kid.UI.Mark(&ui.Kid, o, forLayout)
+	if marked {
+		if forLayout {
+			if self.Layout == duit.Clean {
+				self.Layout = duit.DirtyKid
+			}
+		} else {
+			if self.Layout == duit.Clean {
+				self.Draw = duit.DirtyKid
+			}
+		}
+	}
+	return
+}
+
+func (ui *Scroll) Print(self *duit.Kid, indent int) {
+	what := fmt.Sprintf("Scroll Offset=%d childR=%v", ui.Offset, ui.childR)
+	duit.PrintUI(what, self, indent)
+	ui.Kid.UI.Print(&ui.Kid, indent+1)
+}
+
+//////////////////////
+//                  //
+// helper functions //
+//                  //
+//////////////////////
+
+func pt(v int) image.Point {
+	return image.Point{v, v}
+}
+
+func rect(p image.Point) image.Rectangle {
+	return image.Rectangle{image.ZP, p}
+}
+
+func extendY(r image.Rectangle, dy int) image.Rectangle {
+	r.Max.Y += dy
+	return r
+}
+
+func insetPt(r image.Rectangle, pad image.Point) image.Rectangle {
+	r.Min = r.Min.Add(pad)
+	r.Max = r.Max.Sub(pad)
+	return r
+}
+
+func outsetPt(r image.Rectangle, pad image.Point) image.Rectangle {
+	r.Min = r.Min.Sub(pad)
+	r.Max = r.Max.Add(pad)
+	return r
+}
+
+func minimum64(a, b int64) int64 {
+	if a < b {
+		return a
+	}
+	return b
+}
+
+func maximum64(a, b int64) int64 {
+	if a > b {
+		return a
+	}
+	return b
+}
+
+func minimum(a, b int) int {
+	if a < b {
+		return a
+	}
+	return b
+}
+
+func maximum(a, b int) int {
+	if a > b {
+		return a
+	}
+	return b
+}
+
+func debugLayout(d *duit.DUI, self *duit.Kid) {
+	if d.DebugLayout > 0 {
+		log.Printf("duit: Layout %T %s layout=%d draw=%d\n", self.UI, self.R, self.Layout, self.Draw)
+	}
+}
+
+func debugDraw(d *duit.DUI, self *duit.Kid) {
+	if d.DebugDraw > 0 {
+		log.Printf("duit: Draw %T %s layout=%d draw=%d\n", self.UI, self.R, self.Layout, self.Draw)
+	}
+}
+
+func duitError(d *duit.DUI, err error, msg string) bool {
+	if err == nil {
+		return false
+	}
+	go func() {
+		d.Error <- fmt.Errorf("%s: %s", msg, err)
+	}()
+	return true
+}
--- a/browser/experimental.go
+++ b/browser/experimental.go
@@ -3,6 +3,7 @@
 import (
 	"fmt"
 	"image"
+	"github.com/psilva261/opossum/browser/duitx"
 	"github.com/psilva261/opossum/domino"
 	"9fans.net/go/draw"
 	"github.com/mjl-/duit"
@@ -66,7 +67,7 @@
 		return false
 	case *Element:
 		return false
-	case *Grid:
+	case *duitx.Grid:
 		return false
 	case *duit.Image:
 		return true
--- a/browser/grid.go
+++ /dev/null
@@ -1,206 +1,0 @@
-package browser
-
-// Original code from github.com/mjl-/duit
-//
-// Copyright 2018 Mechiel Lukkien mechiel@ueber.net
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy of this
-// software and associated documentation files (the "Software"), to deal in the Software
-// without restriction, including without limitation the rights to use, copy, modify, merge,
-// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
-// to whom the Software is furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in all copies or
-// substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
-// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
-// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
-// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-import (
-	"fmt"
-	"image"
-
-	"9fans.net/go/draw"
-	"github.com/mjl-/duit"
-)
-
-// Grid lays out other duit.UIs in a table-like grid.
-type Grid struct {
-	Kids       []*duit.Kid      // Holds UIs in the grid, per row.
-	Columns    int         // Number of clumns.
-	Valign     []duit.Valign    // Vertical alignment per column.
-	Halign     []duit.Halign    // Horizontal alignment per column.
-	Padding    []duit.Space     // Padding in lowDPI pixels per column.
-	Width      int         // -1 means full width, 0 means automatic width, >0 means exactly that many lowDPI pixels.
-	Background *draw.Image `json:"-"` // Background color.
-
-	widths  []int
-	heights []int
-	size    image.Point
-}
-
-var _ duit.UI = &Grid{}
-
-func (ui *Grid) Layout(dui *duit.DUI, self *duit.Kid, sizeAvail image.Point, force bool) {
-	debugLayout(dui, self)
-	if duit.KidsLayout(dui, self, ui.Kids, force) {
-		return
-	}
-
-	if ui.Valign != nil && len(ui.Valign) != ui.Columns {
-		panic(fmt.Sprintf("len(valign) = %d, should be ui.Columns = %d", len(ui.Valign), ui.Columns))
-	}
-	if ui.Halign != nil && len(ui.Halign) != ui.Columns {
-		panic(fmt.Sprintf("len(halign) = %d, should be ui.Columns = %d", len(ui.Halign), ui.Columns))
-	}
-	if ui.Padding != nil && len(ui.Padding) != ui.Columns {
-		panic(fmt.Sprintf("len(padding) = %d, should be ui.Columns = %d", len(ui.Padding), ui.Columns))
-	}
-	if len(ui.Kids)%ui.Columns != 0 {
-		panic(fmt.Sprintf("len(kids) = %d, should be multiple of ui.Columns = %d", len(ui.Kids), ui.Columns))
-	}
-
-	scaledWidth := dui.Scale(ui.Width)
-	if scaledWidth > 0 && scaledWidth < sizeAvail.X {
-		ui.size.X = scaledWidth
-	}
-
-	ui.widths = make([]int, ui.Columns) // widths include padding
-	spaces := make([]duit.Space, ui.Columns)
-	if ui.Padding != nil {
-		for i, pad := range ui.Padding {
-			spaces[i] = dui.ScaleSpace(pad)
-		}
-	}
-	width := 0                       // total width so far
-	x := make([]int, len(ui.widths)) // x offsets per column
-	x[0] = 0
-
-	// first determine the column widths
-	for col := 0; col < ui.Columns; col++ {
-		if col > 0 {
-			x[col] = x[col-1] + ui.widths[col-1]
-		}
-		ui.widths[col] = 0
-		newDx := 0
-		space := spaces[col]
-		for i := col; i < len(ui.Kids); i += ui.Columns {
-			k := ui.Kids[i]
-			k.UI.Layout(dui, k, image.Pt(sizeAvail.X-width-space.Dx(), sizeAvail.Y-space.Dy()), true)
-			newDx = maximum(newDx, k.R.Dx()+space.Dx())
-		}
-		ui.widths[col] = newDx
-		width += ui.widths[col]
-	}
-	if scaledWidth < 0 && width < sizeAvail.X {
-		leftover := sizeAvail.X - width
-		given := 0
-		for i := range ui.widths {
-			x[i] += given
-			var dx int
-			if i == len(ui.widths)-1 {
-				dx = leftover - given
-			} else {
-				dx = leftover / len(ui.widths)
-			}
-			ui.widths[i] += dx
-			given += dx
-		}
-	}
-
-	// now determine row heights
-	ui.heights = make([]int, (len(ui.Kids)+ui.Columns-1)/ui.Columns)
-	height := 0                       // total height so far
-	y := make([]int, len(ui.heights)) // including padding
-	y[0] = 0
-	for i := 0; i < len(ui.Kids); i += ui.Columns {
-		row := i / ui.Columns
-		if row > 0 {
-			y[row] = y[row-1] + ui.heights[row-1]
-		}
-		rowDy := 0
-		for col := 0; col < ui.Columns; col++ {
-			space := spaces[col]
-			k := ui.Kids[i+col]
-			k.UI.Layout(dui, k, image.Pt(ui.widths[col]-space.Dx(), sizeAvail.Y-y[row]-space.Dy()), true)
-			offset := image.Pt(x[col], y[row]).Add(space.Topleft())
-			k.R = k.R.Add(offset) // aligned in top left, fixed for halign/valign later on
-			rowDy = maximum(rowDy, k.R.Dy()+space.Dy())
-		}
-		ui.heights[row] = rowDy
-		height += ui.heights[row]
-	}
-
-	// now shift the kids for right valign/halign
-	for i, k := range ui.Kids {
-		row := i / ui.Columns
-		col := i % ui.Columns
-		space := spaces[col]
-
-		valign := duit.ValignTop
-		halign := duit.HalignLeft
-		if ui.Valign != nil {
-			valign = ui.Valign[col]
-		}
-		if ui.Halign != nil {
-			halign = ui.Halign[col]
-		}
-		cellSize := image.Pt(ui.widths[col], ui.heights[row]).Sub(space.Size())
-		spaceX := 0
-		switch halign {
-		case duit.HalignLeft:
-		case duit.HalignMiddle:
-			spaceX = (cellSize.X - k.R.Dx()) / 2
-		case duit.HalignRight:
-			spaceX = cellSize.X - k.R.Dx()
-		}
-		spaceY := 0
-		switch valign {
-		case duit.ValignTop:
-		case duit.ValignMiddle:
-			spaceY = (cellSize.Y - k.R.Dy()) / 2
-		case duit.ValignBottom:
-			spaceY = cellSize.Y - k.R.Dy()
-		}
-		k.R = k.R.Add(image.Pt(spaceX, spaceY))
-	}
-
-	ui.size = image.Pt(width, height)
-	if ui.Width < 0 && ui.size.X < sizeAvail.X {
-		ui.size.X = sizeAvail.X
-	}
-	self.R = rect(ui.size)
-}
-
-func (ui *Grid) Draw(dui *duit.DUI, self *duit.Kid, img *draw.Image, orig image.Point, m draw.Mouse, force bool) {
-	duit.KidsDraw(dui, self, ui.Kids, ui.size, ui.Background, img, orig, m, force)
-}
-
-func (ui *Grid) Mouse(dui *duit.DUI, self *duit.Kid, m draw.Mouse, origM draw.Mouse, orig image.Point) (r duit.Result) {
-	return duit.KidsMouse(dui, self, ui.Kids, m, origM, orig)
-}
-
-func (ui *Grid) Key(dui *duit.DUI, self *duit.Kid, k rune, m draw.Mouse, orig image.Point) (r duit.Result) {
-	return duit.KidsKey(dui, self, ui.Kids, k, m, orig)
-}
-
-func (ui *Grid) FirstFocus(dui *duit.DUI, self *duit.Kid) *image.Point {
-	return duit.KidsFirstFocus(dui, self, ui.Kids)
-}
-
-func (ui *Grid) Focus(dui *duit.DUI, self *duit.Kid, o duit.UI) *image.Point {
-	return duit.KidsFocus(dui, self, ui.Kids, o)
-}
-
-func (ui *Grid) Mark(self *duit.Kid, o duit.UI, forLayout bool) (marked bool) {
-	return duit.KidsMark(self, ui.Kids, o, forLayout)
-}
-
-func (ui *Grid) Print(self *duit.Kid, indent int) {
-	duit.PrintUI(fmt.Sprintf("Grid columns=%d padding=%v", ui.Columns, ui.Padding), self, indent)
-	duit.KidsPrint(ui.Kids, indent+1)
-}
--- a/browser/scroll.go
+++ /dev/null
@@ -1,449 +1,0 @@
-package browser
-
-// Original code from github.com/mjl-/duit
-//
-// Copyright 2018 Mechiel Lukkien mechiel@ueber.net
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy of this
-// software and associated documentation files (the "Software"), to deal in the Software
-// without restriction, including without limitation the rights to use, copy, modify, merge,
-// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
-// to whom the Software is furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in all copies or
-// substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
-// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
-// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
-// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-import (
-	"fmt"
-	"image"
-	"math"
-
-	"9fans.net/go/draw"
-	"github.com/mjl-/duit"
-)
-
-// Scroll shows a part of its single child, typically a box, and lets you scroll the content.
-type Scroll struct {
-	Kid    duit.Kid
-	Height int // < 0 means full height, 0 means as much as necessary, >0 means exactly that many lowdpi pixels
-
-	r             image.Rectangle // entire ui
-	barR          image.Rectangle
-	barActiveR    image.Rectangle
-	childR        image.Rectangle
-	Offset        int         // current scroll offset in pixels
-	img           *draw.Image // for child to draw on
-	scrollbarSize int
-	lastMouseUI   duit.UI
-	drawOffset int
-}
-
-var _ duit.UI = &Scroll{}
-
-// NewScroll returns a full-height scroll bar containing ui.
-func NewScroll(ui duit.UI) *Scroll {
-	return &Scroll{Height: -1, Kid: duit.Kid{UI: ui}}
-}
-
-func (ui *Scroll) Layout(dui *duit.DUI, self *duit.Kid, sizeAvail image.Point, force bool) {
-	debugLayout(dui, self)
-
-	if self.Layout == duit.Clean && !force {
-		return
-	}
-	self.Layout = duit.Clean
-	self.Draw = duit.Dirty
-	// todo: be smarter about DirtyKid
-
-	ui.scrollbarSize = dui.Scale(duit.ScrollbarSize)
-	scaledHeight := dui.Scale(ui.Height)
-	if scaledHeight > 0 && scaledHeight < sizeAvail.Y {
-		sizeAvail.Y = scaledHeight
-	}
-	ui.r = rect(sizeAvail)
-	ui.barR = ui.r
-	ui.barR.Max.X = ui.barR.Min.X + ui.scrollbarSize
-	ui.childR = ui.r
-	ui.childR.Min.X = ui.barR.Max.X
-
-	// todo: only force when sizeAvail or childR changed?
-	ui.Kid.UI.Layout(dui, &ui.Kid, image.Pt(ui.r.Dx()-ui.barR.Dx(), ui.r.Dy()), force)
-	ui.Kid.Layout = duit.Clean
-	ui.Kid.Draw = duit.Dirty
-
-	kY := ui.Kid.R.Dy()
-	if ui.r.Dy() > kY && ui.Height == 0 {
-		ui.barR.Max.Y = kY
-		ui.r.Max.Y = kY
-		ui.childR.Max.Y = kY
-	}
-	self.R = rect(ui.r.Size())
-}
-
-func (ui *Scroll) Draw(dui *duit.DUI, self *duit.Kid, img *draw.Image, orig image.Point, m draw.Mouse, force bool) {
-	debugDraw(dui, self)
-
-	if self.Draw == duit.Clean {
-		return
-	}
-	self.Draw = duit.Clean
-
-	if ui.r.Empty() {
-		return
-	}
-
-	// ui.scroll(0)
-	barHover := m.In(ui.barR)
-
-	bg := dui.ScrollBGNormal
-	vis := dui.ScrollVisibleNormal
-	if barHover {
-		bg = dui.ScrollBGHover
-		vis = dui.ScrollVisibleHover
-	}
-
-	h := ui.r.Dy()
-	uih := ui.Kid.R.Dy()
-	if uih > h {
-		barR := ui.barR.Add(orig)
-		img.Draw(barR, bg, nil, image.ZP)
-		barH := h * h / uih
-		barY := ui.Offset * h / uih
-		ui.barActiveR = ui.barR
-		ui.barActiveR.Min.Y += barY
-		ui.barActiveR.Max.Y = ui.barActiveR.Min.Y + barH
-		barActiveR := ui.barActiveR.Add(orig)
-		barActiveR.Max.X -= 1 // unscaled
-		img.Draw(barActiveR, vis, nil, image.ZP)
-	}
-
-	// draw child ui
-	if ui.childR.Empty() {
-		return
-	}
-	d := math.Abs(float64(ui.drawOffset - ui.Offset))
-	if  d > float64(ui.r.Max.Y) {
-		ui.Kid.Draw = duit.Dirty
-	}
-	if ui.img == nil || ui.drawRect().Size() != ui.img.R.Size() || ui.Kid.Draw == duit.Dirty {
-		var err error
-		if ui.img != nil {
-			ui.img.Free()
-			ui.img = nil
-		}
-		ui.Kid.Draw = duit.Dirty
-		if ui.Kid.R.Dx() == 0 || ui.Kid.R.Dy() == 0 {
-			return
-		}
-		ui.img, err = dui.Display.AllocImage(ui.drawRect(), draw.ARGB32, false, dui.BackgroundColor)
-		if duitError(dui, err, "allocimage") {
-			return
-		}
-		ui.drawOffset = ui.Offset
-	} else if ui.Kid.Draw == duit.Dirty {
-		ui.img.Draw(ui.img.R, dui.Background, nil, image.ZP)
-	}
-	m.Point = m.Point.Add(image.Pt(-ui.childR.Min.X, ui.Offset))
-	if ui.Kid.Draw != duit.Clean {
-		if force {
-			ui.Kid.Draw = duit.Dirty
-		}
-		ui.Kid.UI.Draw(dui, &ui.Kid, ui.img, image.ZP, m, ui.Kid.Draw == duit.Dirty)
-		ui.Kid.Draw = duit.Clean
-	}
-	img.Draw(ui.childR.Add(orig), ui.img, nil, image.Pt(0, ui.Offset))
-}
-
-// Allocate only an image buffer of view size ui.r
-// - which is translated by scroll offset ui.Offset - instead
-// of whole Kid view size ui.Kid.R which leads to much
-// faster render times for large pages. Add same size rectangles
-// above/below to decrease flickering.
-func (ui *Scroll) drawRect() image.Rectangle {
-	if 2*ui.r.Dy() > ui.Kid.R.Dy() {
-		return ui.Kid.R
-	}
-	r := image.Rectangle{
-		Min: ui.r.Min,
-		Max: image.Point{
-			ui.r.Max.X,
-			3*ui.r.Max.Y,
-		},
-	}
-	r = r.Add(image.Point{X:0, Y:ui.Offset-ui.r.Max.Y})
-	if r.Min.Y > ui.Offset {
-		r.Min.Y -= ui.Offset
-	}
-	return r
-}
-
-func (ui *Scroll) scroll(delta int) (changed bool) {
-	o := ui.Offset
-	ui.Offset += delta
-	ui.Offset = maximum(0, ui.Offset)
-	ui.Offset = minimum(ui.Offset, maximum(0, ui.Kid.R.Dy()-ui.childR.Dy()))
-	return o != ui.Offset
-}
-
-func (ui *Scroll) scrollKey(k rune) (consumed bool) {
-	switch k {
-	case draw.KeyUp:
-		return ui.scroll(-50)
-	case draw.KeyDown:
-		return ui.scroll(50)
-	case draw.KeyPageUp:
-		return ui.scroll(-200)
-	case draw.KeyPageDown:
-		return ui.scroll(200)
-	}
-	return false
-}
-
-func (ui *Scroll) scrollMouse(m draw.Mouse, scrollOnly bool) (consumed bool) {
-	switch m.Buttons {
-	case duit.Button4:
-		return ui.scroll(-m.Y / 4)
-	case duit.Button5:
-		return ui.scroll(m.Y / 4)
-	}
-
-	if scrollOnly {
-		return false
-	}
-	switch m.Buttons {
-	case duit.Button1:
-		return ui.scroll(-m.Y)
-	case duit.Button2:
-		Offset := m.Y * ui.Kid.R.Dy() / ui.barR.Dy()
-		OffsetMax := ui.Kid.R.Dy() - ui.childR.Dy()
-		Offset = maximum(0, minimum(Offset, OffsetMax))
-		o := ui.Offset
-		ui.Offset = Offset
-		return o != ui.Offset
-	case duit.Button3:
-		return ui.scroll(m.Y)
-	}
-	return false
-}
-
-func (ui *Scroll) result(dui *duit.DUI, self *duit.Kid, r *duit.Result, scrolled bool) {
-	if ui.Kid.Layout != duit.Clean {
-		ui.Kid.UI.Layout(dui, &ui.Kid, ui.childR.Size(), false)
-		ui.Kid.Layout = duit.Clean
-		ui.Kid.Draw = duit.Dirty
-		self.Draw = duit.Dirty
-	} else if ui.Kid.Draw != duit.Clean || scrolled {
-		self.Draw = duit.Dirty
-	}
-}
-
-func (ui *Scroll) Mouse(dui *duit.DUI, self *duit.Kid, m draw.Mouse, origM draw.Mouse, orig image.Point) (r duit.Result) {
-	nOrigM := origM
-	nOrigM.Point = nOrigM.Point.Add(image.Pt(-ui.scrollbarSize, ui.Offset))
-	nm := m
-	nm.Point = nm.Point.Add(image.Pt(-ui.scrollbarSize, ui.Offset))
-
-	if m.Buttons == 0 {
-		ui.Kid.UI.Mouse(dui, &ui.Kid, nm, nOrigM, image.ZP)
-		return
-	}
-	if m.Point.In(ui.barR) {
-		r.Hit = ui
-		r.Consumed = ui.scrollMouse(m, false)
-		self.Draw = duit.Dirty
-		return
-	} else if m.Point.In(ui.childR) {
-		r.Consumed = ui.scrollMouse(m, true)
-		if r.Consumed {
-			self.Draw = duit.Dirty
-			return
-		}
-		r = ui.Kid.UI.Mouse(dui, &ui.Kid, nm, nOrigM, image.ZP)
-		if r.Consumed {
-			self.Draw = duit.Dirty
-		}
-	}
-	return
-}
-
-func (ui *Scroll) Key(dui *duit.DUI, self *duit.Kid, k rune, m draw.Mouse, orig image.Point) (r duit.Result) {
-	if m.Point.In(ui.barR) {
-		r.Hit = ui
-		r.Consumed = ui.scrollKey(k)
-		if r.Consumed {
-			self.Draw = duit.Dirty
-		}
-	}
-	if m.Point.In(ui.childR) {
-		m.Point = m.Point.Add(image.Pt(-ui.scrollbarSize, ui.Offset))
-		r = ui.Kid.UI.Key(dui, &ui.Kid, k, m, image.ZP)
-		ui.warpScroll(dui, self, r.Warp, orig)
-		scrolled := false
-		if !r.Consumed {
-			scrolled = ui.scrollKey(k)
-			r.Consumed = scrolled
-		}
-		ui.result(dui, self, &r, scrolled)
-	}
-	return
-}
-
-func (ui *Scroll) warpScroll(dui *duit.DUI, self *duit.Kid, warp *image.Point, orig image.Point) {
-	if warp == nil {
-		return
-	}
-
-	Offset := ui.Offset
-	if warp.Y < ui.Offset {
-		ui.Offset = maximum(0, warp.Y-dui.Scale(40))
-	} else if warp.Y > ui.Offset+ui.r.Dy() {
-		ui.Offset = minimum(ui.Kid.R.Dy()-ui.r.Dy(), warp.Y+dui.Scale(40)-ui.r.Dy())
-	}
-	if Offset != ui.Offset {
-		if self != nil {
-			self.Draw = duit.Dirty
-		} else {
-			dui.MarkDraw(ui)
-		}
-	}
-	warp.Y -= ui.Offset
-	warp.X += orig.X + ui.scrollbarSize
-	warp.Y += orig.Y
-}
-
-func (ui *Scroll) _focus(dui *duit.DUI, p *image.Point) *image.Point {
-	if p == nil {
-		return nil
-	}
-	pp := p.Add(ui.childR.Min)
-	p = &pp
-	ui.warpScroll(dui, nil, p, image.ZP)
-	return p
-}
-
-func (ui *Scroll) FirstFocus(dui *duit.DUI, self *duit.Kid) *image.Point {
-	p := ui.Kid.UI.FirstFocus(dui, &ui.Kid)
-	return ui._focus(dui, p)
-}
-
-func (ui *Scroll) Focus(dui *duit.DUI, self *duit.Kid, o duit.UI) *image.Point {
-	if o == ui {
-		p := image.Pt(minimum(ui.scrollbarSize/2, ui.r.Dx()), minimum(ui.scrollbarSize/2, ui.r.Dy()))
-		return &p
-	}
-	p := ui.Kid.UI.Focus(dui, &ui.Kid, o)
-	return ui._focus(dui, p)
-}
-
-func (ui *Scroll) Mark(self *duit.Kid, o duit.UI, forLayout bool) (marked bool) {
-	if self.Mark(o, forLayout) {
-		return true
-	}
-	marked = ui.Kid.UI.Mark(&ui.Kid, o, forLayout)
-	if marked {
-		if forLayout {
-			if self.Layout == duit.Clean {
-				self.Layout = duit.DirtyKid
-			}
-		} else {
-			if self.Layout == duit.Clean {
-				self.Draw = duit.DirtyKid
-			}
-		}
-	}
-	return
-}
-
-func (ui *Scroll) Print(self *duit.Kid, indent int) {
-	what := fmt.Sprintf("Scroll Offset=%d childR=%v", ui.Offset, ui.childR)
-	duit.PrintUI(what, self, indent)
-	ui.Kid.UI.Print(&ui.Kid, indent+1)
-}
-
-//////////////////////
-//                  //
-// helper functions //
-//                  //
-//////////////////////
-
-func pt(v int) image.Point {
-	return image.Point{v, v}
-}
-
-func rect(p image.Point) image.Rectangle {
-	return image.Rectangle{image.ZP, p}
-}
-
-func extendY(r image.Rectangle, dy int) image.Rectangle {
-	r.Max.Y += dy
-	return r
-}
-
-func insetPt(r image.Rectangle, pad image.Point) image.Rectangle {
-	r.Min = r.Min.Add(pad)
-	r.Max = r.Max.Sub(pad)
-	return r
-}
-
-func outsetPt(r image.Rectangle, pad image.Point) image.Rectangle {
-	r.Min = r.Min.Sub(pad)
-	r.Max = r.Max.Add(pad)
-	return r
-}
-
-func minimum64(a, b int64) int64 {
-	if a < b {
-		return a
-	}
-	return b
-}
-
-func maximum64(a, b int64) int64 {
-	if a > b {
-		return a
-	}
-	return b
-}
-
-func minimum(a, b int) int {
-	if a < b {
-		return a
-	}
-	return b
-}
-
-func maximum(a, b int) int {
-	if a > b {
-		return a
-	}
-	return b
-}
-
-func debugLayout(d *duit.DUI, self *duit.Kid) {
-	if d.DebugLayout > 0 {
-		log.Printf("duit: Layout %T %s layout=%d draw=%d\n", self.UI, self.R, self.Layout, self.Draw)
-	}
-}
-
-func debugDraw(d *duit.DUI, self *duit.Kid) {
-	if d.DebugDraw > 0 {
-		log.Printf("duit: Draw %T %s layout=%d draw=%d\n", self.UI, self.R, self.Layout, self.Draw)
-	}
-}
-
-func duitError(d *duit.DUI, err error, msg string) bool {
-	if err == nil {
-		return false
-	}
-	go func() {
-		d.Error <- fmt.Errorf("%s: %s", msg, err)
-	}()
-	return true
-}
--- a/browser/website.go
+++ b/browser/website.go
@@ -7,6 +7,7 @@
 	"io/ioutil"
 	"net/url"
 	"github.com/psilva261/opossum"
+	"github.com/psilva261/opossum/browser/duitx"
 	"github.com/psilva261/opossum/browser/fs"
 	"github.com/psilva261/opossum/domino"
 	"github.com/psilva261/opossum/nodes"
@@ -134,7 +135,7 @@
 
 	log.Printf("Layout website...")
 	nt := nodes.NewNodeTree(body, style.Map{}, nodeMap, &nodes.Node{})
-	scroller = NewScroll(
+	scroller = duitx.NewScroll(
 		NodeToBox(0, browser, nt),
 	)
 	numElements := 0
@@ -146,7 +147,7 @@
 	if numElements < 10 {
 		log.Errorf("Less than 10 elements layouted, seems css processing failed. Will layout without css")
 		nt = nodes.NewNodeTree(body, style.Map{}, make(map[*html.Node]style.Map), nil)
-		scroller = NewScroll(
+		scroller = duitx.NewScroll(
 			NodeToBox(0, browser, nt),
 		)
 		w.UI = scroller