diff --git a/backends/x/entity.go b/backends/x/entity.go index 6252d55..78053f3 100644 --- a/backends/x/entity.go +++ b/backends/x/entity.go @@ -10,18 +10,25 @@ type entity struct { children []*entity element tomo.Element - drawDirty bool - layoutDirty bool - - bounds image.Rectangle - minWidth int - minHeight int + bounds image.Rectangle + clippedBounds image.Rectangle + minWidth int + minHeight int + + layoutInvalid bool + isContainer bool } -func bind (element tomo.Element) *entity { - entity := &entity { drawDirty: true } +func bind (parent *entity, window *window, element tomo.Element) *entity { + entity := &entity { + window: window, + parent: parent, + element: element, + } + entity.Invalidate() if _, ok := element.(tomo.Container); ok { - entity.layoutDirty = true + entity.isContainer = true + entity.InvalidateLayout() } element.Bind(entity) @@ -38,7 +45,8 @@ func (entity *entity) unbind () { // ----------- Entity ----------- // func (entity *entity) Invalidate () { - entity.drawDirty = true + if entity.window.system.invalidateIgnore { return } + entity.window.drawingInvalid.Add(entity) } func (entity *entity) Bounds () image.Rectangle { @@ -64,18 +72,20 @@ func (entity *entity) DrawBackground (destination canvas.Canvas, bounds image.Re // ----------- ContainerEntity ----------- // func (entity *entity) InvalidateLayout () { - entity.layoutDirty = true + if !entity.isContainer { return } + entity.layoutInvalid = true + entity.window.system.anyLayoutInvalid = true } func (entity *entity) Adopt (child tomo.Element) { - entity.children = append(entity.children, bind(child)) + entity.children = append(entity.children, bind(entity, entity.window, child)) } func (entity *entity) Insert (index int, child tomo.Element) { entity.children = append ( entity.children[:index + 1], entity.children[index:]...) - entity.children[index] = bind(child) + entity.children[index] = bind(entity, entity.window, child) } func (entity *entity) Disown (index int) { @@ -104,7 +114,13 @@ func (entity *entity) CountChildren () int { } func (entity *entity) PlaceChild (index int, bounds image.Rectangle) { - entity.children[index].bounds = bounds + child := entity.children[index] + child.bounds = bounds + child.clippedBounds = entity.bounds.Intersect(bounds) + child.Invalidate() + if child.isContainer { + child.InvalidateLayout() + } } func (entity *entity) ChildMinimumSize (index int) (width, height int) { diff --git a/backends/x/event.go b/backends/x/event.go index 4675c2f..a796bb1 100644 --- a/backends/x/event.go +++ b/backends/x/event.go @@ -41,7 +41,6 @@ func (sum *scrollSum) add (button xproto.Button, window *window, state uint16) { sum.x += scrollDistance } } - } func (window *window) handleExpose ( @@ -49,6 +48,7 @@ func (window *window) handleExpose ( event xevent.ExposeEvent, ) { _, region := window.compressExpose(*event.ExposeEvent) + window.system.afterEvent() window.pushRegion(region) } @@ -74,7 +74,6 @@ func (window *window) handleConfigureNotify ( window.updateBounds ( configureEvent.X, configureEvent.Y, configureEvent.Width, configureEvent.Height) - if sizeChanged { configureEvent = window.compressConfigureNotify(configureEvent) @@ -85,8 +84,11 @@ func (window *window) handleConfigureNotify ( window.resizeChildToFit() if !window.exposeEventFollows(configureEvent) { - window.redrawChildEntirely() + window.child.Invalidate() + window.child.InvalidateLayout() } + + window.system.afterEvent() } } @@ -136,21 +138,21 @@ func (window *window) handleKeyPress ( modifiers.NumberPad = numberPad if key == input.KeyTab && modifiers.Alt { - if child, ok := window.child.(tomo.Focusable); ok { - direction := input.KeynavDirectionForward - if modifiers.Shift { - direction = input.KeynavDirectionBackward - } - - if !child.HandleFocus(direction) { - child.HandleUnfocus() - } - } + // if child, ok := window.child.element.(tomo.Focusable); ok { + // direction := input.KeynavDirectionForward + // if modifiers.Shift { + // direction = input.KeynavDirectionBackward + // } +// + // // TODO + // } } else if key == input.KeyEscape && window.shy { window.Close() - } else if child, ok := window.child.(tomo.KeyboardTarget); ok { + } else if child, ok := window.child.element.(tomo.KeyboardTarget); ok { child.HandleKeyDown(key, modifiers) } + + window.system.afterEvent() } func (window *window) handleKeyRelease ( @@ -182,9 +184,11 @@ func (window *window) handleKeyRelease ( modifiers := window.modifiersFromState(keyEvent.State) modifiers.NumberPad = numberPad - if child, ok := window.child.(tomo.KeyboardTarget); ok { + if child, ok := window.child.element.(tomo.KeyboardTarget); ok { child.HandleKeyUp(key, modifiers) } + + window.system.afterEvent() } func (window *window) handleButtonPress ( @@ -205,7 +209,7 @@ func (window *window) handleButtonPress ( if !insideWindow && window.shy && !scrolling { window.Close() } else if scrolling { - if child, ok := window.child.(tomo.ScrollTarget); ok { + if child, ok := window.child.element.(tomo.ScrollTarget); ok { sum := scrollSum { } sum.add(buttonEvent.Detail, window, buttonEvent.State) window.compressScrollSum(buttonEvent, &sum) @@ -215,7 +219,7 @@ func (window *window) handleButtonPress ( float64(sum.x), float64(sum.y)) } } else { - if child, ok := window.child.(tomo.MouseTarget); ok { + if child, ok := window.child.element.(tomo.MouseTarget); ok { child.HandleMouseDown ( int(buttonEvent.EventX), int(buttonEvent.EventY), @@ -223,6 +227,7 @@ func (window *window) handleButtonPress ( } } + window.system.afterEvent() } func (window *window) handleButtonRelease ( @@ -231,7 +236,7 @@ func (window *window) handleButtonRelease ( ) { if window.child == nil { return } - if child, ok := window.child.(tomo.MouseTarget); ok { + if child, ok := window.child.element.(tomo.MouseTarget); ok { buttonEvent := *event.ButtonReleaseEvent if buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 { return } child.HandleMouseUp ( @@ -239,6 +244,8 @@ func (window *window) handleButtonRelease ( int(buttonEvent.EventY), input.Button(buttonEvent.Detail)) } + + window.system.afterEvent() } func (window *window) handleMotionNotify ( @@ -247,12 +254,14 @@ func (window *window) handleMotionNotify ( ) { if window.child == nil { return } - if child, ok := window.child.(tomo.MotionTarget); ok { + if child, ok := window.child.element.(tomo.MotionTarget); ok { motionEvent := window.compressMotionNotify(*event.MotionNotifyEvent) child.HandleMotion ( int(motionEvent.EventX), int(motionEvent.EventY)) } + + window.system.afterEvent() } func (window *window) handleSelectionNotify ( diff --git a/backends/x/system.go b/backends/x/system.go new file mode 100644 index 0000000..6b53011 --- /dev/null +++ b/backends/x/system.go @@ -0,0 +1,109 @@ +package x + +import "image" +import "git.tebibyte.media/sashakoshka/tomo" +import "git.tebibyte.media/sashakoshka/tomo/canvas" + +type entitySet map[*entity] struct { } + +func (set entitySet) Empty () bool { + return len(set) == 0 +} + +func (set entitySet) Has (entity *entity) bool { + _, ok := set[entity] + return ok +} + +func (set entitySet) Add (entity *entity) { + set[entity] = struct { } { } +} + +type system struct { + child *entity + focused *entity + canvas canvas.BasicCanvas + + theme tomo.Theme + config tomo.Config + + invalidateIgnore bool + drawingInvalid entitySet + anyLayoutInvalid bool + + pushFunc func (image.Rectangle) +} + +func (system *system) initialize () { + system.drawingInvalid = make(entitySet) +} + +func (system *system) SetTheme (theme tomo.Theme) { + system.theme = theme + if system.child == nil { return } + if child, ok := system.child.element.(tomo.Themeable); ok { + child.SetTheme(theme) + } +} + +func (system *system) SetConfig (config tomo.Config) { + system.config = config + if system.child == nil { return } + if child, ok := system.child.element.(tomo.Configurable); ok { + child.SetConfig(config) + } +} + +func (system *system) resizeChildToFit () { + system.child.bounds = system.canvas.Bounds() + system.child.clippedBounds = system.child.bounds + system.child.Invalidate() + if system.child.isContainer { + system.child.InvalidateLayout() + } +} + +func (system *system) afterEvent () { + if system.anyLayoutInvalid { + system.layout(system.child, false) + system.anyLayoutInvalid = false + } + system.draw() +} + +func (system *system) layout (entity *entity, force bool) { + if entity == nil { return } + if entity.layoutInvalid == true || force { + entity.element.(tomo.Container).Layout() + entity.layoutInvalid = false + force = true + } + + for _, child := range entity.children { + system.layout(child, force) + } +} + +func (system *system) draw () { + finalBounds := image.Rectangle { } + + // ignore invalidations that result from drawing elements, because if an + // element decides to do that it really needs to rethink its life + // choices. + system.invalidateIgnore = true + defer func () { system.invalidateIgnore = false } () + + for entity := range system.drawingInvalid { + entity.element.Draw (canvas.Cut ( + system.canvas, + entity.clippedBounds)) + finalBounds = finalBounds.Union(entity.clippedBounds) + } + system.drawingInvalid = make(entitySet) + + // TODO: don't just union all the bounds together, we can definetly + // consolidateupdated regions more efficiently than this. + if !finalBounds.Empty() { + system.pushFunc(finalBounds) + } +} diff --git a/backends/x/window.go b/backends/x/window.go index 0253994..eb661ac 100644 --- a/backends/x/window.go +++ b/backends/x/window.go @@ -13,20 +13,16 @@ import "github.com/jezek/xgbutil/mousebind" import "github.com/jezek/xgbutil/xgraphics" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/data" -import "git.tebibyte.media/sashakoshka/tomo/input" import "git.tebibyte.media/sashakoshka/tomo/canvas" -// import "runtime/debug" type mainWindow struct { *window } type menuWindow struct { *window } type window struct { + system + backend *Backend xWindow *xwindow.Window xCanvas *xgraphics.Image - canvas canvas.BasicCanvas - child *entity - focused *entity - onClose func () title, application string @@ -34,15 +30,14 @@ type window struct { hasModal bool shy bool - theme tomo.Theme - config tomo.Config - selectionRequest *selectionRequest selectionClaim *selectionClaim metrics struct { bounds image.Rectangle } + + onClose func () } func (backend *Backend) NewWindow ( @@ -53,6 +48,10 @@ func (backend *Backend) NewWindow ( ) { if backend == nil { panic("nil backend") } window, err := backend.newWindow(bounds, false) + + window.system.initialize() + window.system.pushFunc = window.paste + output = mainWindow { window } return output, err } @@ -136,69 +135,24 @@ func (backend *Backend) newWindow ( return } -func (window *window) NotifyMinimumSizeChange (child tomo.Element) { - window.childMinimumSizeChangeCallback(child.MinimumSize()) -} - func (window *window) Window () tomo.Window { return window } -func (window *window) RequestFocus ( - child tomo.Focusable, -) ( - granted bool, -) { - return true -} - -func (window *window) RequestFocusNext (child tomo.Focusable) { - if child, ok := window.child.(tomo.Focusable); ok { - if !child.HandleFocus(input.KeynavDirectionForward) { - child.HandleUnfocus() - } - } -} - -func (window *window) RequestFocusPrevious (child tomo.Focusable) { - if child, ok := window.child.(tomo.Focusable); ok { - if !child.HandleFocus(input.KeynavDirectionBackward) { - child.HandleUnfocus() - } - } -} - func (window *window) Adopt (child tomo.Element) { // disown previous child if window.child != nil { - window.child.SetParent(nil) - window.child.DrawTo(nil, image.Rectangle { }, nil) + window.child.unbind() + window.child = nil } + // adopt new child if child != nil { - // adopt new child - window.child = child - child.SetParent(window) - if newChild, ok := child.(tomo.Themeable); ok { - newChild.SetTheme(window.theme) - } - if newChild, ok := child.(tomo.Configurable); ok { - newChild.SetConfig(window.config) - } - if child != nil { - if !window.childMinimumSizeChangeCallback(child.MinimumSize()) { - window.resizeChildToFit() - window.redrawChildEntirely() - } - } + window.child = bind(nil, window, child) + window.resizeChildToFit() } } -func (window *window) Child () (child tomo.Element) { - child = window.child - return -} - func (window *window) SetTitle (title string) { window.title = title ewmh.WmNameSet ( @@ -317,43 +271,6 @@ func (window menuWindow) Pin () { // TODO iungrab keyboard and mouse } -func (window *window) grabInput () { - keybind.GrabKeyboard(window.backend.connection, window.xWindow.Id) - mousebind.GrabPointer ( - window.backend.connection, - window.xWindow.Id, - window.backend.connection.RootWin(), 0) -} - -func (window *window) ungrabInput () { - keybind.UngrabKeyboard(window.backend.connection) - mousebind.UngrabPointer(window.backend.connection) -} - -func (window *window) inheritProperties (parent *window) { - window.SetApplicationName(parent.application) -} - -func (window *window) setType (ty string) error { - return ewmh.WmWindowTypeSet ( - window.backend.connection, - window.xWindow.Id, - []string { "_NET_WM_WINDOW_TYPE_" + ty }) -} - -func (window *window) setClientLeader (leader *window) error { - hints, _ := icccm.WmHintsGet(window.backend.connection, window.xWindow.Id) - if hints == nil { - hints = &icccm.Hints { } - } - hints.Flags |= icccm.HintWindowGroup - hints.WindowGroup = leader.xWindow.Id - return icccm.WmHintsSet ( - window.backend.connection, - window.xWindow.Id, - hints) -} - func (window *window) Show () { if window.child == nil { window.xCanvas.For (func (x, y int) xgraphics.BGRA { @@ -362,7 +279,7 @@ func (window *window) Show () { window.pushRegion(window.xCanvas.Bounds()) } - + window.xWindow.Map() if window.shy { window.grabInput() } } @@ -417,18 +334,41 @@ func (window *window) OnClose (callback func ()) { window.onClose = callback } -func (window *window) SetTheme (theme tomo.Theme) { - window.theme = theme - if child, ok := window.child.(tomo.Themeable); ok { - child.SetTheme(theme) - } +func (window *window) grabInput () { + keybind.GrabKeyboard(window.backend.connection, window.xWindow.Id) + mousebind.GrabPointer ( + window.backend.connection, + window.xWindow.Id, + window.backend.connection.RootWin(), 0) } -func (window *window) SetConfig (config tomo.Config) { - window.config = config - if child, ok := window.child.(tomo.Configurable); ok { - child.SetConfig(config) +func (window *window) ungrabInput () { + keybind.UngrabKeyboard(window.backend.connection) + mousebind.UngrabPointer(window.backend.connection) +} + +func (window *window) inheritProperties (parent *window) { + window.SetApplicationName(parent.application) +} + +func (window *window) setType (ty string) error { + return ewmh.WmWindowTypeSet ( + window.backend.connection, + window.xWindow.Id, + []string { "_NET_WM_WINDOW_TYPE_" + ty }) +} + +func (window *window) setClientLeader (leader *window) error { + hints, _ := icccm.WmHintsGet(window.backend.connection, window.xWindow.Id) + if hints == nil { + hints = &icccm.Hints { } } + hints.Flags |= icccm.HintWindowGroup + hints.WindowGroup = leader.xWindow.Id + return icccm.WmHintsSet ( + window.backend.connection, + window.xWindow.Id, + hints) } func (window *window) reallocateCanvas () { @@ -464,26 +404,6 @@ func (window *window) reallocateCanvas () { } -func (window *window) redrawChildEntirely () { - window.paste(window.canvas.Bounds()) - window.pushRegion(window.canvas.Bounds()) -} - -func (window *window) resizeChildToFit () { - window.skipChildDrawCallback = true - window.child.DrawTo ( - window.canvas, - window.canvas.Bounds(), - window.childDrawCallback) - window.skipChildDrawCallback = false -} - -func (window *window) childDrawCallback (region image.Rectangle) { - if window.skipChildDrawCallback { return } - window.paste(region) - window.pushRegion(region) -} - func (window *window) paste (region image.Rectangle) { canvas := canvas.Cut(window.canvas, region) data, stride := canvas.Buffer() @@ -492,7 +412,6 @@ func (window *window) paste (region image.Rectangle) { dstStride := window.xCanvas.Stride dstData := window.xCanvas.Pix - // debug.PrintStack() for y := bounds.Min.Y; y < bounds.Max.Y; y ++ { srcYComponent := y * stride dstYComponent := y * dstStride @@ -507,6 +426,18 @@ func (window *window) paste (region image.Rectangle) { } } +func (window *window) pushRegion (region image.Rectangle) { + if window.xCanvas == nil { panic("whoopsie!!!!!!!!!!!!!!") } + image, ok := window.xCanvas.SubImage(region).(*xgraphics.Image) + if ok { + image.XDraw() + image.XExpPaint ( + window.xWindow.Id, + image.Bounds().Min.X, + image.Bounds().Min.Y) + } +} + func (window *window) childMinimumSizeChangeCallback (width, height int) (resized bool) { icccm.WmNormalHintsSet ( window.backend.connection, @@ -528,15 +459,3 @@ func (window *window) childMinimumSizeChangeCallback (width, height int) (resize return false } - -func (window *window) pushRegion (region image.Rectangle) { - if window.xCanvas == nil { panic("whoopsie!!!!!!!!!!!!!!") } - image, ok := window.xCanvas.SubImage(region).(*xgraphics.Image) - if ok { - image.XDraw() - image.XExpPaint ( - window.xWindow.Id, - image.Bounds().Min.X, - image.Bounds().Min.Y) - } -} diff --git a/backends/x/x.go b/backends/x/x.go index 40c3021..ec69708 100644 --- a/backends/x/x.go +++ b/backends/x/x.go @@ -67,6 +67,9 @@ func (backend *Backend) Run () (err error) { <- pingAfter case callback := <- backend.doChannel: callback() + for _, window := range backend.windows { + window.system.afterEvent() + } case <- pingQuit: return } diff --git a/elements/testing/artist.go b/elements/testing/artist.go index cd2d848..91faa0a 100644 --- a/elements/testing/artist.go +++ b/elements/testing/artist.go @@ -26,7 +26,7 @@ func NewArtist () *Artist { func (element *Artist) Bind (entity tomo.Entity) { element.entity = entity - entity.SetMinimumSize(240, 240) + if entity != nil { entity.SetMinimumSize(240, 240) } } func (element *Artist) Draw (destination canvas.Canvas) { diff --git a/window.go b/window.go index 1813a81..a88b2c8 100644 --- a/window.go +++ b/window.go @@ -15,9 +15,6 @@ type Window interface { // these at one time. Adopt (Element) - // Child returns the root element of the window. - Child () Element - // SetTitle sets the title that appears on the window's title bar. This // method might have no effect with some backends. SetTitle (string)