From 3fe5ad316abfb44662f1533a2ddc49e3599eb487 Mon Sep 17 00:00:00 2001 From: Richard Wilkes Date: Wed, 2 Mar 2022 18:56:31 -0800 Subject: [PATCH] Scaling (#4) --- dock_layout.go | 2 + dock_tab.go | 4 +- example/demo/demo.go | 14 ++--- example/demo/demo_row.go | 4 +- flex_layout.go | 2 +- flow_layout.go | 2 +- font_face.go | 2 +- panel.go | 119 ++++++++++++++++++++++++++++++--------- scroll_panel.go | 36 +++++++----- table.go | 11 ++-- table_header.go | 11 ++-- tooltip.go | 2 +- window.go | 3 +- 13 files changed, 140 insertions(+), 72 deletions(-) diff --git a/dock_layout.go b/dock_layout.go index a295c25..b585463 100644 --- a/dock_layout.go +++ b/dock_layout.go @@ -15,6 +15,8 @@ import ( "github.com/richardwilkes/toolbox/xmath/mathf32" ) +// TODO: Fix scaling for docks, too + var ( _ Layout = &DockLayout{} _ DockLayoutNode = &DockLayout{} diff --git a/dock_tab.go b/dock_tab.go index dcf1178..4a60d11 100644 --- a/dock_tab.go +++ b/dock_tab.go @@ -179,13 +179,13 @@ func (t *dockTab) attemptClose() { } } -func (t *dockTab) updateTooltip(where geom32.Point, suggestedAvoid geom32.Rect) geom32.Rect { +func (t *dockTab) updateTooltip(where geom32.Point, suggestedAvoidInRoot geom32.Rect) geom32.Rect { if tip := t.dockable.Tooltip(); tip != "" { t.Tooltip = NewTooltipWithText(t.dockable.Tooltip()) } else { t.Tooltip = nil } - return suggestedAvoid + return suggestedAvoidInRoot } func (t *dockTab) mouseDown(where geom32.Point, button, clickCount int, mod Modifiers) bool { diff --git a/example/demo/demo.go b/example/demo/demo.go index 1c4436a..03dde3d 100644 --- a/example/demo/demo.go +++ b/example/demo/demo.go @@ -176,7 +176,6 @@ func createButtonsPanel() *unison.Panel { btn.SetEnabled(false) } } - return panel } @@ -543,14 +542,13 @@ func createImagePanel() *unison.Label { imgPanel.UpdateCursorCallback = func(where geom32.Point) *unison.Cursor { return cursor } // Add a tooltip that shows the current mouse coordinates - imgPanel.UpdateTooltipCallback = func(where geom32.Point, avoid geom32.Rect) geom32.Rect { + imgPanel.UpdateTooltipCallback = func(where geom32.Point, suggestedAvoidInRoot geom32.Rect) geom32.Rect { imgPanel.Tooltip = unison.NewTooltipWithText(where.String()) - avoid.X = where.X - 16 - avoid.Y = where.Y - 16 - avoid.Point = imgPanel.PointToRoot(avoid.Point) - avoid.Width = 32 - avoid.Height = 32 - return avoid + suggestedAvoidInRoot.X = where.X - 16 + suggestedAvoidInRoot.Y = where.Y - 16 + suggestedAvoidInRoot.Width = 32 + suggestedAvoidInRoot.Height = 32 + return imgPanel.RectToRoot(suggestedAvoidInRoot) } // Set the initial image diff --git a/example/demo/demo_row.go b/example/demo/demo_row.go index f9988e1..3cb0b94 100644 --- a/example/demo/demo_row.go +++ b/example/demo/demo_row.go @@ -76,9 +76,9 @@ func (d *demoRow) ColumnCell(row, col int, selected bool) unison.Paneler { if d.doubleHeight { addWrappedText(wrapper, "A little noteā€¦", unison.LabelFont.Face().Font(unison.LabelFont.Size()-1), width, selected) } - wrapper.UpdateTooltipCallback = func(where geom32.Point, suggestedAvoid geom32.Rect) geom32.Rect { + wrapper.UpdateTooltipCallback = func(where geom32.Point, suggestedAvoidInRoot geom32.Rect) geom32.Rect { wrapper.Tooltip = unison.NewTooltipWithText("A tooltip for the cell") - return wrapper.RectToRoot(wrapper.FrameRect()) + return wrapper.RectToRoot(wrapper.ContentRect(true)) } return wrapper case 2: diff --git a/flex_layout.go b/flex_layout.go index b207571..ca05697 100644 --- a/flex_layout.go +++ b/flex_layout.go @@ -60,7 +60,7 @@ func (f *FlexLayout) PerformLayout(target *Panel) { if b := target.Border(); b != nil { insets = b.Insets() } - hint := target.FrameRect().Size + hint := target.ContentRect(true).Size hint.SubtractInsets(insets) f.layout(target, geom32.Point{X: insets.Left, Y: insets.Top}, hint, true, false) } diff --git a/flow_layout.go b/flow_layout.go index 0096935..4129b8b 100644 --- a/flow_layout.go +++ b/flow_layout.go @@ -116,7 +116,7 @@ func (f *FlowLayout) PerformLayout(target *Panel) { if b := target.Border(); b != nil { insets = b.Insets() } - size := target.FrameRect().Size + size := target.ContentRect(true).Size width := size.Width - (insets.Left + insets.Right) pt := geom32.Point{X: insets.Left, Y: insets.Top} availWidth := width diff --git a/font_face.go b/font_face.go index b0b0217..b79754d 100644 --- a/font_face.go +++ b/font_face.go @@ -93,7 +93,7 @@ func (f *FontFace) Font(capHeightSizeInLogicalPixels float32) Font { } skiaSize++ } - for font.metrics.CapHeight > capHeightSizeInLogicalPixels { + for skiaSize >= 1 && font.metrics.CapHeight > capHeightSizeInLogicalPixels { skiaSize -= 0.5 font = f.createFontWithSkiaSize(skiaSize) } diff --git a/panel.go b/panel.go index bddb094..36a7b87 100644 --- a/panel.go +++ b/panel.go @@ -31,7 +31,7 @@ type Panel struct { DrawCallback func(gc *Canvas, rect geom32.Rect) DrawOverCallback func(gc *Canvas, rect geom32.Rect) UpdateCursorCallback func(where geom32.Point) *Cursor - UpdateTooltipCallback func(where geom32.Point, suggestedAvoid geom32.Rect) geom32.Rect + UpdateTooltipCallback func(where geom32.Point, suggestedAvoidInRoot geom32.Rect) geom32.Rect CanPerformCmdCallback func(source interface{}, id int) bool PerformCmdCallback func(source interface{}, id int) FrameChangeCallback func() @@ -57,6 +57,7 @@ type Panel struct { layoutData interface{} children []*Panel data map[string]interface{} + scale float32 NeedsLayout bool focusable bool disabled bool @@ -203,13 +204,34 @@ func (p *Panel) Window() *Window { } } +// Scale returns the scale that has been applied to this panel. This will be automatically applied, transforming the +// graphics and mouse events. +func (p *Panel) Scale() float32 { + if p.scale <= 0 { // This happens if not explicitly set. 0 or less isn't valid, so make it 1 + p.scale = 1 + } + return p.scale +} + +// SetScale sets the scale for this panel and the panels in the hierarchy below it. +func (p *Panel) SetScale(scale float32) { + p.scale = scale +} + // FrameRect returns the location and size of the panel in its parent's coordinate system. func (p *Panel) FrameRect() geom32.Rect { - return p.frame + scale := p.Scale() + r := p.frame + r.Width *= scale + r.Height *= scale + return r } // SetFrameRect sets the location and size of the panel in its parent's coordinate system. func (p *Panel) SetFrameRect(rect geom32.Rect) { + scale := p.Scale() + rect.Width /= scale + rect.Height /= scale moved := p.frame.X != rect.X || p.frame.Y != rect.Y resized := p.frame.Width != rect.Width || p.frame.Height != rect.Height if moved || resized { @@ -271,13 +293,24 @@ func (p *Panel) SetSizer(sizer Sizer) { // panel's layout. If no layout is present, then the panel's sizer is asked. If no sizer is present, then it finally // uses a default set of sizes that are used for all panels. func (p *Panel) Sizes(hint geom32.Size) (min, pref, max geom32.Size) { - if p.layout != nil { - return p.layout.LayoutSizes(p, hint) - } - if p.sizer != nil { - return p.sizer(hint) - } - return geom32.Size{}, geom32.Size{}, geom32.Size{Width: DefaultMaxSize, Height: DefaultMaxSize} + scale := p.Scale() + hint.Width /= scale + hint.Height /= scale + switch { + case p.layout != nil: + min, pref, max = p.layout.LayoutSizes(p, hint) + case p.sizer != nil: + min, pref, max = p.sizer(hint) + default: + return min, pref, geom32.Size{Width: DefaultMaxSize, Height: DefaultMaxSize} + } + min.Width *= scale + min.Height *= scale + pref.Width *= scale + pref.Height *= scale + max.Width *= scale + max.Height *= scale + return } // Layout returns the Layout for this panel, if any. @@ -343,9 +376,11 @@ func (p *Panel) Draw(gc *Canvas, rect geom32.Rect) { if p.Hidden { return } - rect.Intersect(p.ContentRect(true)) + rect.Intersect(p.frame.CopyAndZeroLocation()) if !rect.IsEmpty() { gc.Save() + scale := p.Scale() + gc.Scale(scale, scale) gc.ClipRect(rect, IntersectClipOp, false) if p.DrawCallback != nil { gc.Save() @@ -356,12 +391,17 @@ func (p *Panel) Draw(gc *Canvas, rect geom32.Rect) { for i := len(p.children) - 1; i >= 0; i-- { if child := p.children[i]; !child.Hidden { adjusted := rect - adjusted.Intersect(child.frame) + childFrame := child.FrameRect() + adjusted.Intersect(childFrame) if !adjusted.IsEmpty() { gc.Save() - gc.Translate(child.frame.X, child.frame.Y) - adjusted.X -= child.frame.X - adjusted.Y -= child.frame.Y + gc.Translate(childFrame.X, childFrame.Y) + adjusted.Point.Subtract(childFrame.Point) + scale = child.Scale() + adjusted.X /= scale + adjusted.Y /= scale + adjusted.Width /= scale + adjusted.Height /= scale child.Draw(gc, adjusted) gc.Restore() } @@ -422,9 +462,14 @@ func (p *Panel) RequestFocus() { // PanelAt returns the leaf-most child panel containing the point, or this panel if no child is found. func (p *Panel) PanelAt(pt geom32.Point) *Panel { for _, child := range p.children { - if !child.Hidden && child.frame.ContainsPoint(pt) { - pt.Subtract(child.frame.Point) - return child.PanelAt(pt) + if !child.Hidden { + if r := child.FrameRect(); r.ContainsPoint(pt) { + pt.Subtract(r.Point) + scale := child.Scale() + pt.X /= scale + pt.Y /= scale + return child.PanelAt(pt) + } } } return p @@ -433,11 +478,13 @@ func (p *Panel) PanelAt(pt geom32.Point) *Panel { // PointToRoot converts panel-local coordinates into root coordinates, which when rooted within a window, will be // window-local coordinates. func (p *Panel) PointToRoot(pt geom32.Point) geom32.Point { - pt.Add(p.frame.Point) - parent := p.parent - for parent != nil { - pt.Add(parent.frame.Point) - parent = parent.parent + one := p + for one != nil { + scale := one.Scale() + pt.X *= scale + pt.Y *= scale + pt.Add(one.frame.Point) + one = one.parent } return pt } @@ -445,11 +492,18 @@ func (p *Panel) PointToRoot(pt geom32.Point) geom32.Point { // PointFromRoot converts root coordinates (i.e. window-local, when rooted within a window) into panel-local // coordinates. func (p *Panel) PointFromRoot(pt geom32.Point) geom32.Point { - pt.Subtract(p.frame.Point) - parent := p.parent - for parent != nil { - pt.Subtract(parent.frame.Point) - parent = parent.parent + list := make([]*Panel, 0, 32) + one := p + for one != nil { + list = append(list, one) + one = one.parent + } + for i := len(list) - 1; i >= 0; i-- { + one = list[i] + pt.Subtract(one.frame.Point) + scale := one.Scale() + pt.X /= scale + pt.Y /= scale } return pt } @@ -457,13 +511,19 @@ func (p *Panel) PointFromRoot(pt geom32.Point) geom32.Point { // RectToRoot converts panel-local coordinates into root coordinates, which when rooted within a window, will be // window-local coordinates. func (p *Panel) RectToRoot(rect geom32.Rect) geom32.Rect { + pt := p.PointToRoot(rect.BottomRight()) rect.Point = p.PointToRoot(rect.Point) + rect.Width = pt.X - rect.X + rect.Height = pt.Y - rect.Y return rect } // RectFromRoot converts root coordinates (i.e. window-local, when rooted within a window) into panel-local coordinates. func (p *Panel) RectFromRoot(rect geom32.Rect) geom32.Rect { + pt := p.PointFromRoot(rect.BottomRight()) rect.Point = p.PointFromRoot(rect.Point) + rect.Width = pt.X - rect.X + rect.Height = pt.Y - rect.Y return rect } @@ -474,7 +534,7 @@ func (p *Panel) ScrollIntoView() { } // ScrollRectIntoView attempts to scroll the rect (in coordinates local to this Panel) into the current view if it is -// not already there, using ScrollAreas in this Panel's hierarchy. +// not already there, using scroll areas in this Panel's hierarchy. func (p *Panel) ScrollRectIntoView(rect geom32.Rect) { look := p for look != nil { @@ -483,6 +543,9 @@ func (p *Panel) ScrollRectIntoView(rect geom32.Rect) { return } } + scale := look.Scale() + rect.X *= scale + rect.Y *= scale rect.Point.Add(look.frame.Point) look = look.parent } diff --git a/scroll_panel.go b/scroll_panel.go index c2b9316..847a319 100644 --- a/scroll_panel.go +++ b/scroll_panel.go @@ -70,8 +70,8 @@ func NewScrollPanel() *ScrollPanel { s.AddChild(s.verticalBar) s.AddChild(s.contentView) s.SetLayout(s) - s.horizontalBar.ChangedCallback = s.barChanged - s.verticalBar.ChangedCallback = s.barChanged + s.horizontalBar.ChangedCallback = s.Sync + s.verticalBar.ChangedCallback = s.Sync s.DrawCallback = s.DefaultDraw s.MouseWheelCallback = s.DefaultMouseWheel s.ScrollRectIntoViewCallback = s.DefaultScrollRectIntoView @@ -109,7 +109,7 @@ func (s *ScrollPanel) SetColumnHeader(p Paneler) { s.AddChild(s.columnHeaderView) } s.columnHeaderView.AddChild(p) - s.barChanged() + s.Sync() } else if s.columnHeaderView != nil { s.columnHeaderView.RemoveFromParent() s.columnHeaderView = nil @@ -139,7 +139,7 @@ func (s *ScrollPanel) SetRowHeader(p Paneler) { s.AddChild(s.rowHeaderView) } s.rowHeaderView.AddChild(p) - s.barChanged() + s.Sync() } else if s.rowHeaderView != nil { s.rowHeaderView.RemoveFromParent() s.rowHeaderView = nil @@ -166,7 +166,7 @@ func (s *ScrollPanel) SetContent(p Paneler, behave Behavior) { s.behavior = behave if p != nil { s.contentView.AddChild(p) - s.barChanged() + s.Sync() } s.MarkForLayoutAndRedraw() } @@ -188,23 +188,27 @@ func (s *ScrollPanel) DefaultDraw(canvas *Canvas, dirty geom32.Rect) { canvas.DrawRect(r, s.BackgroundInk.Paint(canvas, r, Fill)) } -func (s *ScrollPanel) barChanged() { +// Sync the headers and content with the current scroll state. +func (s *ScrollPanel) Sync() { if s.columnHeader != nil { - r := s.columnHeader.AsPanel().ContentRect(true) + r := s.columnHeader.AsPanel().FrameRect() r.X = -s.horizontalBar.Value() + r.Y = 0 s.columnHeader.AsPanel().SetFrameRect(r) } if s.rowHeader != nil { - r := s.rowHeader.AsPanel().ContentRect(true) + r := s.rowHeader.AsPanel().FrameRect() + r.X = 0 r.Y = -s.verticalBar.Value() s.rowHeader.AsPanel().SetFrameRect(r) } if s.content != nil { - r := s.content.AsPanel().ContentRect(true) + r := s.content.AsPanel().FrameRect() r.X = -s.horizontalBar.Value() r.Y = -s.verticalBar.Value() s.content.AsPanel().SetFrameRect(r) } + s.PerformLayout(s.AsPanel()) } // DefaultMouseWheel provides the default mouse wheel handling. @@ -228,7 +232,9 @@ func (s *ScrollPanel) DefaultMouseWheel(where, delta geom32.Point, mod Modifiers // DefaultScrollRectIntoView provides the default scroll rect into contentView handling. func (s *ScrollPanel) DefaultScrollRectIntoView(rect geom32.Rect) bool { - viewRect := s.contentView.ContentRect(false) + viewRect := s.contentView.FrameRect() + viewRect.X = 0 + viewRect.Y = 0 if s.columnHeaderView != nil { height := s.columnHeaderView.FrameRect().Height viewRect.Y += height @@ -270,7 +276,7 @@ func computeScrollAdj(contentTopLeft, viewTopLeft, contentBottomRight, viewBotto func (s *ScrollPanel) DefaultFrameChangeInChildHierarchy(panel *Panel) { // TODO: Need to adjust the headers, too? if s.content != nil { - vs := s.contentView.ContentRect(false).Size + vs := s.contentView.FrameRect().Size r := s.content.AsPanel().FrameRect() nl := r.Point if r.Y != 0 && vs.Height > r.Bottom() { @@ -339,7 +345,9 @@ func (s *ScrollPanel) LayoutSizes(_ *Panel, hint geom32.Size) (min, pref, max ge // PerformLayout implements the Layout interface. func (s *ScrollPanel) PerformLayout(_ *Panel) { - r := s.ContentRect(false) + r := s.FrameRect() + r.X = 0 + r.Y = 0 columnHeaderTop := r.Y if s.columnHeaderView != nil { _, p, _ := s.columnHeader.AsPanel().Sizes(geom32.Size{Width: r.Width}) @@ -431,14 +439,14 @@ func (s *ScrollPanel) PerformLayout(_ *Panel) { s.horizontalBar.SetFrameRect(geom32.NewRect(viewContent.X, viewContent.Bottom()-s.horizontalBar.MinimumThickness, width, s.horizontalBar.MinimumThickness)) s.contentView.SetFrameRect(r) if s.columnHeaderView != nil { - vr := s.columnHeaderView.ContentRect(false) + vr := s.columnHeaderView.FrameRect() r = s.columnHeader.AsPanel().FrameRect() r.Height = vr.Height r.Width = mathf32.Max(vr.Width, contentSize.Width) s.columnHeader.AsPanel().SetFrameRect(r) } if s.rowHeaderView != nil { - vr := s.rowHeaderView.ContentRect(false) + vr := s.rowHeaderView.FrameRect() r = s.rowHeader.AsPanel().FrameRect() r.Width = vr.Width r.Height = mathf32.Max(vr.Height, contentSize.Height) diff --git a/table.go b/table.go index 6b0c68b..17b03e7 100644 --- a/table.go +++ b/table.go @@ -454,7 +454,7 @@ func (t *Table) DefaultUpdateCursorCallback(where geom32.Point) *Cursor { } // DefaultUpdateTooltipCallback provides the default tooltip update handling. -func (t *Table) DefaultUpdateTooltipCallback(where geom32.Point, suggestedAvoid geom32.Rect) geom32.Rect { +func (t *Table) DefaultUpdateTooltipCallback(where geom32.Point, suggestedAvoidInRoot geom32.Rect) geom32.Rect { if row := t.OverRow(where.Y); row != -1 { if col := t.OverColumn(where.X); col != -1 { cell := t.rowCache[row].row.ColumnCell(row, col, t.IsRowOrAnyParentSelected(row)).AsPanel() @@ -463,17 +463,16 @@ func (t *Table) DefaultUpdateTooltipCallback(where geom32.Point, suggestedAvoid t.installCell(cell, rect) where.Subtract(rect.Point) var avoid geom32.Rect - toolbox.Call(func() { avoid = cell.UpdateTooltipCallback(where, suggestedAvoid) }) + toolbox.Call(func() { avoid = cell.UpdateTooltipCallback(where, suggestedAvoidInRoot) }) t.Tooltip = cell.Tooltip t.uninstallCell(cell) return avoid } if cell.Tooltip != nil { t.Tooltip = cell.Tooltip - suggestedAvoid = t.CellFrame(row, col) - suggestedAvoid.Point = t.PointToRoot(suggestedAvoid.Point) - suggestedAvoid.Align() - return suggestedAvoid + suggestedAvoidInRoot = t.RectToRoot(t.CellFrame(row, col)) + suggestedAvoidInRoot.Align() + return suggestedAvoidInRoot } } } diff --git a/table_header.go b/table_header.go index f549f9f..0e30656 100644 --- a/table_header.go +++ b/table_header.go @@ -221,14 +221,14 @@ func (h *TableHeader) DefaultUpdateCursorCallback(where geom32.Point) *Cursor { } // DefaultUpdateTooltipCallback provides the default tooltip update handling. -func (h *TableHeader) DefaultUpdateTooltipCallback(where geom32.Point, suggestedAvoid geom32.Rect) geom32.Rect { +func (h *TableHeader) DefaultUpdateTooltipCallback(where geom32.Point, suggestedAvoidInRoot geom32.Rect) geom32.Rect { if col := h.Table.OverColumn(where.X); col != -1 { cell := h.ColumnHeaders[col].AsPanel() if cell.UpdateTooltipCallback != nil { rect := h.ColumnFrame(col) h.installCell(cell, rect) where.Subtract(rect.Point) - rect.Point = h.PointToRoot(rect.Point) + rect = h.RectToRoot(rect) rect.Align() avoid := cell.UpdateTooltipCallback(where, rect) h.Tooltip = cell.Tooltip @@ -237,10 +237,9 @@ func (h *TableHeader) DefaultUpdateTooltipCallback(where geom32.Point, suggested } if cell.Tooltip != nil { h.Tooltip = cell.Tooltip - suggestedAvoid = h.ColumnFrame(col) - suggestedAvoid.Point = h.PointToRoot(suggestedAvoid.Point) - suggestedAvoid.Align() - return suggestedAvoid + suggestedAvoidInRoot = h.RectToRoot(h.ColumnFrame(col)) + suggestedAvoidInRoot.Align() + return suggestedAvoidInRoot } } h.Tooltip = nil diff --git a/tooltip.go b/tooltip.go index b9720e9..d854038 100644 --- a/tooltip.go +++ b/tooltip.go @@ -99,7 +99,7 @@ func (ts *tooltipSequencer) show() { if ts.window.tooltipSequence == ts.sequence && ts.window.Focused() { tip := ts.window.lastTooltip _, pref, _ := tip.Sizes(geom32.Size{}) - rect := geom32.Rect{Point: geom32.Point{X: ts.avoid.X, Y: ts.avoid.Y + ts.avoid.Height + 1}, Size: pref} + rect := geom32.Rect{Point: geom32.Point{X: ts.avoid.X, Y: ts.avoid.Bottom() + 1}, Size: pref} if rect.X < 0 { rect.X = 0 } diff --git a/window.go b/window.go index d47daa0..f5ff5fd 100644 --- a/window.go +++ b/window.go @@ -878,8 +878,7 @@ func (w *Window) updateTooltip(target *Panel, where geom32.Point) { var avoid geom32.Rect var tip *Panel for target != nil { - avoid = target.ContentRect(true) - avoid.Point = target.PointToRoot(avoid.Point) + avoid = target.RectToRoot(target.ContentRect(true)) avoid.Align() if target.UpdateTooltipCallback != nil { toolbox.Call(func() { avoid = target.UpdateTooltipCallback(target.PointFromRoot(where), avoid) })