Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow a theme to override the form factor of the tabs to be mobile #5239

Merged
merged 4 commits into from
Nov 17, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions container/apptabs.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ func (t *AppTabs) SetItems(items []*TabItem) {

// SetTabLocation sets the location of the tab bar
func (t *AppTabs) SetTabLocation(l TabLocation) {
t.location = tabsAdjustedLocation(l)
t.location = tabsAdjustedLocation(l, t)
t.Refresh()
}

Expand Down Expand Up @@ -357,7 +357,7 @@ func (r *appTabsRenderer) buildTabButtons(count int) *fyne.Container {
buttons := &fyne.Container{}

var iconPos buttonIconPosition
if fyne.CurrentDevice().IsMobile() {
if isMobile(r.tabs) {
cells := count
if cells == 0 {
cells = 1
Expand All @@ -381,6 +381,7 @@ func (r *appTabsRenderer) buildTabButtons(count int) *fyne.Container {
if item.button == nil {
item.button = &tabButton{
onTapped: func() { r.appTabs.Select(item) },
tabs: r.tabs,
}
}
button := item.button
Expand Down
35 changes: 35 additions & 0 deletions container/apptabs_override_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//go:build !mobile

package container_test

import (
"testing"

"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/test"
"fyne.io/fyne/v2/widget"
)

func TestAppTabs_OverrideMobile(t *testing.T) {
test.NewTempApp(t)

item1 := &container.TabItem{Text: "Test1", Content: widget.NewLabel("Text 1")}
item2 := &container.TabItem{Text: "Test2", Content: widget.NewLabel("Text 2")}
item3 := &container.TabItem{Text: "Test3", Content: widget.NewLabel("Text 3")}
tabs := container.NewAppTabs(item1, item2, item3)
w := test.NewWindow(tabs)
defer w.Close()
w.SetPadded(false)
c := w.Canvas()

min := tabs.MinSize()
w.Resize(min)

test.AssertRendersToMarkup(t, "apptabs/desktop/tab_location_top.xml", c)

override := container.NewThemeOverride(tabs, test.Theme())
override.SetDeviceIsMobile(true)
w.Resize(min.AddWidthHeight(-4, -0))

test.AssertRendersToMarkup(t, "apptabs/mobile/tab_location_top.xml", c)
}
3 changes: 2 additions & 1 deletion container/doctabs.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ func (t *DocTabs) SetItems(items []*TabItem) {

// SetTabLocation sets the location of the tab bar
func (t *DocTabs) SetTabLocation(l TabLocation) {
t.location = tabsAdjustedLocation(l)
t.location = tabsAdjustedLocation(l, t)
t.Refresh()
}

Expand Down Expand Up @@ -338,6 +338,7 @@ func (r *docTabsRenderer) buildTabButtons(count int, buttons *fyne.Container) {
item.button = &tabButton{
onTapped: func() { r.docTabs.Select(item) },
onClosed: func() { r.docTabs.close(item) },
tabs: r.tabs,
}
}
button := item.button
Expand Down
21 changes: 17 additions & 4 deletions container/tabs.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"fyne.io/fyne/v2/driver/desktop"
"fyne.io/fyne/v2/internal"
"fyne.io/fyne/v2/internal/build"
intTheme "fyne.io/fyne/v2/internal/theme"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
Expand Down Expand Up @@ -91,10 +92,20 @@ type baseTabs interface {
setTransitioning(bool)
}

func tabsAdjustedLocation(l TabLocation) TabLocation {
func isMobile(b baseTabs) bool {
d := fyne.CurrentDevice()
mobile := intTheme.FeatureForWidget(intTheme.FeatureNameDeviceIsMobile, b)
if is, ok := mobile.(bool); ok {
return is
}

return d.IsMobile()
}

func tabsAdjustedLocation(l TabLocation, b baseTabs) TabLocation {
// Mobile has limited screen space, so don't put app tab bar on long edges
if d := fyne.CurrentDevice(); d.IsMobile() {
if o := d.Orientation(); fyne.IsVertical(o) {
if isMobile(b) {
if o := fyne.CurrentDevice().Orientation(); fyne.IsVertical(o) {
if l == TabLocationLeading {
return TabLocationTop
} else if l == TabLocationTrailing {
Expand Down Expand Up @@ -511,6 +522,8 @@ type tabButton struct {
onClosed func()
text string
textAlignment fyne.TextAlign

tabs baseTabs
}

func (b *tabButton) CreateRenderer() fyne.WidgetRenderer {
Expand Down Expand Up @@ -720,7 +733,7 @@ func (r *tabButtonRenderer) Refresh() {
r.icon.Hide()
}

if d := fyne.CurrentDevice(); r.button.onClosed != nil && (d.IsMobile() || r.button.hovered || r.close.hovered) {
if r.button.onClosed != nil && (isMobile(r.button.tabs) || r.button.hovered || r.close.hovered) {
r.close.Show()
} else {
r.close.Hide()
Expand Down
37 changes: 34 additions & 3 deletions container/theme.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package container
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/internal/cache"
intTheme "fyne.io/fyne/v2/internal/theme"
"fyne.io/fyne/v2/widget"
)

Expand All @@ -18,6 +19,8 @@ type ThemeOverride struct {
Theme fyne.Theme

holder *fyne.Container

mobile bool
}

// NewThemeOverride provides a container where the child widgets are themed by the specified theme.
Expand All @@ -32,13 +35,13 @@ func NewThemeOverride(obj fyne.CanvasObject, th fyne.Theme) *ThemeOverride {
t := &ThemeOverride{Content: obj, Theme: th, holder: NewStack(obj)}
t.ExtendBaseWidget(t)

cache.OverrideTheme(obj, th)
cache.OverrideTheme(obj, addFeatures(th, t))
obj.Refresh() // required as the widgets passed in could have been initially rendered with default theme
return t
}

func (t *ThemeOverride) CreateRenderer() fyne.WidgetRenderer {
cache.OverrideTheme(t.Content, t.Theme)
cache.OverrideTheme(t.Content, addFeatures(t.Theme, t))

return widget.NewSimpleRenderer(t.holder)
}
Expand All @@ -49,6 +52,34 @@ func (t *ThemeOverride) Refresh() {
t.holder.Refresh()
}

cache.OverrideTheme(t.Content, t.Theme)
cache.OverrideTheme(t.Content, addFeatures(t.Theme, t))
t.BaseWidget.Refresh()
}

// SetDeviceIsMobile allows a ThemeOverride container to shape the contained widgets as a mobile device.
// This will impact containers such as AppTabs and DocTabs, and more in the future, to display a layout
// that would automatically be used for a mobile device runtime.
//
// Since: 2.6
func (t *ThemeOverride) SetDeviceIsMobile(on bool) {
t.mobile = on
t.BaseWidget.Refresh()
}

type featureTheme struct {
fyne.Theme

over *ThemeOverride
}

func addFeatures(th fyne.Theme, o *ThemeOverride) fyne.Theme {
return &featureTheme{Theme: th, over: o}
}

func (f *featureTheme) Feature(n intTheme.FeatureName) any {
if n == intTheme.FeatureNameDeviceIsMobile {
return f.over.mobile
}

return nil
}
33 changes: 33 additions & 0 deletions internal/theme/feature.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package theme

import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/internal/cache"
)

type FeatureName string

const FeatureNameDeviceIsMobile = FeatureName("deviceIsMobile")

// FeatureTheme defines the method to look up features that we use internally to apply functional
// differences through a theme override.
//
// Since: 2.6
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this supposed to be a public API (since it has the Since: line)? If so, this should be in the public theme package rather than internal?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it wasn't - that doc was just over-enthusiastic

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

type FeatureTheme interface {
Feature(FeatureName) any
}

// FeatureForWidget looks up the specified feature flag for the requested widget using the current theme.
// This is for internal purposes and will do nothing if the theme has not been overridden with the
// ThemeOverride container.
//
// Since: 2.6
func FeatureForWidget(name FeatureName, w fyne.Widget) any {
if custom := cache.WidgetTheme(w); custom != nil {
if f, ok := custom.(FeatureTheme); ok {
return f.Feature(name)
}
}

return nil
}