Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
35 changes: 35 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,9 @@ jobs:
- "linux-x11-gtk3"
- "linux-wayland-gtk3"
- "linux-wayland-gtk4"
- "linux-wayland-gtk4-adw"
- "linux-wayland-qt"
- "linux-x11-qt"
Comment on lines +225 to +227
Copy link
Contributor Author

Choose a reason for hiding this comment

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

FYI -- the Qt things worked before because Qt overrode all the variables in the include section.

- "android"
- "iOS"
- "textual-linux"
Expand Down Expand Up @@ -335,6 +338,38 @@ jobs:
setup-python: false # Use the system Python packages
app-user-data-path: "$HOME/.local/share/testbed"

- backend: "linux-wayland-gtk4-adw"
platform: "linux"
runs-on: "ubuntu-24.04"
env:
XDG_RUNTIME_DIR: "/tmp"
# The package list should be build on the same base as unix-prerequisites.md,
# and the BeeWare tutorial. Additional packages will be added for window
# management, and features such as web views and geolocation that aren't part
# of the default/tutorial environment.
pre-command: |
sudo apt update -y
sudo apt install -y --no-install-recommends \
mutter pkg-config python3-dev libgirepository-2.0-dev libcairo2-dev \
gir1.2-webkit-6.0 gir1.2-xapp-1.0 gir1.2-geoclue-2.0 gir1.2-flatpak-1.0 \
gir1.2-gtk-4.0 gir1.2-adw-1

# Start Virtual X Server
echo "Start X server..."
Xvfb :99 -screen 0 2048x1536x24 &
sleep 1

# Start Window Manager
echo "Start window manager..."
# mutter is being run inside a virtual X server because mutter's headless
# mode does not provide a Gdk.Display
DISPLAY=:99 MUTTER_DEBUG_DUMMY_MODE_SPECS=2048x1536 \
mutter --nested --wayland --no-x11 --wayland-display toga &
sleep 1
briefcase-run-prefix: "WAYLAND_DISPLAY=toga TOGA_GTK=4 TOGA_GTKLIB=Adw"
setup-python: false # Use the system Python packages
app-user-data-path: "$HOME/.local/share/testbed"

- backend: "linux-x11-qt"
platform: "linux"
runs-on: "ubuntu-24.04"
Expand Down
1 change: 1 addition & 0 deletions changes/3069.feature.3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The GTK4 backend now integrates with libadwaita for ActivityIndicator, Window, and App.
2 changes: 2 additions & 0 deletions docs/en/reference/platforms/linux/gtk-prerequisites.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ If you're not using one of these, you'll need to work out how to install the dev

In addition to the dependencies above, if you would like to help add additional support for GTK4, you need to also install `gir1.2-gtk-4.0` on Ubuntu/Debian, or `gtk4` on Fedora or Arch. For other distributions, consult your distribution's platform documentation.

If you would like to run the GTK4 backend with libadwaita on GNOME, install `gir1.2-adw-1` on Ubuntu/Debian or `libadwaita` on Fedora or Arch. For other distributions, consult your distribution's platform documentation.

Some widgets (most notably, the [WebView][webview-system-requires] and [MapView][mapview-system-requires] widgets) have additional system requirements. Likewise, certain hardware features ([Location][location-system-requires]) have system requirements.

See the documentation of those widgets and hardware features for details.
10 changes: 9 additions & 1 deletion docs/en/reference/platforms/linux/gtk.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ $ python -m pip install toga-gtk

### GTK 4 support (experimental)

The experimental GTK 4 backend requires the use of GTK 4.8 or newer. This requirement is met by Debian 12, Ubuntu 24.04, and Fedora 41. Most testing occurs with GTK 4.14, as this is the version that ships with Ubuntu 24.04.
The experimental GTK 4 backend requires the use of GTK 4.10 or newer. This requirement is met by Debian 13, Ubuntu 24.04, and Fedora 41. Most testing occurs with GTK 4.14, as this is the version that ships with Ubuntu 24.04.

If you want to use the experimental GTK 4 backend, run:

Expand All @@ -55,6 +55,14 @@ and set the `TOGA_GTK` environment variable:
$ export TOGA_GTK=4
```

The experimental GTK 4 backend also aims to provides support for integrating with desktop environment-specific libraries. At present, `libadwaita` is the only supported library of this kind. This functionality requires libadwaita 1.5 or newer. To enable libadwaita integration support, set:
Copy link
Contributor Author

Choose a reason for hiding this comment

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

libadwaita 1.5 changed the way dialog things are handled, and also the application "about" dialogs. The relevant APIs are deprecated in libadwaita 1.6.

All major distros ship libadwaita 1.5+. Most major distros ship libadwaita 1.6+.

```console
$ export TOGA_GTKLIB=Adw
```
Most testing occurs with libadwaita 1.5, as this is the version that ships with Ubuntu 24.04.

Integration with libhelium of tauOS and Granite of elementaryOS is planned.

## Implementation details

The `toga-gtk` backend uses the [GTK3 API](https://docs.gtk.org/gtk3/). The experimental GTK 4 `toga-gtk` backend uses the [GTK4 API](https://docs.gtk.org/gtk4/).
Expand Down
4 changes: 4 additions & 0 deletions docs/spelling_wordlist
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ DOM
dotfiles
dp
draggable
elementaryOS
Embedder
Flatpak
Flexbox
Expand Down Expand Up @@ -89,6 +90,8 @@ iOS
iterable
KDE
KWin
libadwaita
libhelium
linters
ListSource
macOS
Expand Down Expand Up @@ -183,6 +186,7 @@ SwipeRefreshLayout
taAppLister
TableViews
taRpnCalcTG
tauOS
testbed
TestPyPI
TextInput
Expand Down
20 changes: 12 additions & 8 deletions gtk/src/toga_gtk/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
GTK_VERSION,
IS_WAYLAND,
TOGA_DEFAULT_STYLES,
Adw,
Gdk,
Gio,
GLib,
Expand All @@ -34,17 +35,20 @@ def __init__(self, interface):
self.loop = self.policy.get_event_loop()

# Stimulate the build of the app
# *Note* -- the coverage may be inaccurate if GTK3 is used with
# a newer version of glib or if GTK4 is used with an older version
# of glib. On local runs, coverage errors here can be safely
# ignored if the version of software is as described above.
if GLIB_VERSION < (2, 74, 0): # pragma: no-cover-if-gtk4
self.native = Gtk.Application(
if Adw is None: # pragma: cover-if-plain-gtk
Application = Gtk.Application
else: # pragma: cover-if-libadwaita
Application = Adw.Application
# *Note* -- the coverage may be inaccurate on older
# glib versions; it is safe to ignore when running
# locally.
if GLIB_VERSION < (2, 74, 0): # pragma: no cover
self.native = Application(
application_id=self.interface.app_id,
flags=Gio.ApplicationFlags.FLAGS_NONE,
)
else: # pragma: no-cover-if-gtk3
self.native = Gtk.Application(
else:
self.native = Application(
application_id=self.interface.app_id,
flags=Gio.ApplicationFlags.DEFAULT_FLAGS,
)
Expand Down
21 changes: 19 additions & 2 deletions gtk/src/toga_gtk/libs/gtk.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@
gi.require_version("Gdk", gtk_version)
gi.require_version("Gtk", gtk_version)

if (
gtk_version == "4.0" and os.getenv("TOGA_GTKLIB") == "Adw"
): # pragma: cover-if-libadwaita
gi.require_version("Adw", "1")
from gi.repository import Adw # noqa: E402, F401
else: # pragma: cover-if-plain-gtk
Adw = None

from gi.events import GLibEventLoopPolicy # noqa: E402, F401
from gi.repository import ( # noqa: E402, F401
Gdk,
Expand All @@ -25,10 +33,19 @@

GLIB_VERSION: tuple[int, int, int] = (
GLib.MAJOR_VERSION,
GLib.MAJOR_VERSION,
GLib.MAJOR_VERSION,
GLib.MINOR_VERSION,
GLib.MICRO_VERSION,
)
Comment on lines 34 to 38
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I can't believe I didn't catch this before...


if Adw: # pragma: cover-if-libadwaita
ADW_VERSION: tuple[int, int, int] = (
Adw.get_major_version(),
Adw.get_minor_version(),
Adw.get_micro_version(),
)
else: # pragma: cover-if-plain-gtk
ADW_VERSION = None

if GTK_VERSION < (4, 0, 0): # pragma: no-cover-if-gtk4
default_display = Gdk.Screen.get_default()
else: # pragma: no-cover-if-gtk3
Expand Down
94 changes: 63 additions & 31 deletions gtk/src/toga_gtk/widgets/activityindicator.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,67 @@
from ..libs import GTK_VERSION, Gtk
from ..libs import ADW_VERSION, GTK_VERSION, Adw, Gtk
from .base import Widget


class ActivityIndicator(Widget):
def create(self):
self.native = Gtk.Spinner()

def is_running(self):
if GTK_VERSION < (4, 0, 0): # pragma: no-cover-if-gtk4
return self.native.get_property("active")
else: # pragma: no-cover-if-gtk3
return self.native.get_property("spinning")

def start(self):
self.native.start()

def stop(self):
self.native.stop()

def rehint(self):
if GTK_VERSION < (4, 0, 0): # pragma: no-cover-if-gtk4
# print(
# "REHINT",
# self,
# self.native.get_preferred_width(),
# self.native.get_preferred_height(),
# )
width = self.native.get_preferred_width()[0]
height = self.native.get_preferred_height()[0]
else: # pragma: no-cover-if-gtk3
size = self.native.get_preferred_size()[0]
width, height = size.width, size.height
self.interface.intrinsic.width = width
self.interface.intrinsic.height = height
# libadwaita 1.6.0 is not in Ubuntu 24.04 yet; no-cover it.
if Adw is not None and ADW_VERSION >= (1, 6, 0): # pragma: no cover
Comment on lines +6 to +7
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Does this no-cover perhaps makes you nervous? Should we land Adwaita 1.6.0 changes until all majors distros ship it? Right now Ubuntu 24.04 does not.


def create(self):
self.native = Adw.Spinner()
self._hidden = False
self._running = False

def set_hidden(self, hidden):
super().set_hidden(not self._running or hidden)
self._hidden = hidden

def start(self):
self._running = True
super().set_hidden(self._hidden)

def stop(self):
self._running = False
super().set_hidden(True)

def is_running(self):
return self._running

def rehint(self):
# libadwaita spinners could take on any size;
# getting preferred size would not work. Hardcode
# a reasonable size based on documented limits.
self.interface.intrinsic.width = 32
self.interface.intrinsic.height = 32

else: # pragma: cover-if-plain-gtk

def create(self):
self.native = Gtk.Spinner()

def start(self):
self.native.start()

def stop(self):
self.native.stop()

def is_running(self):
if GTK_VERSION < (4, 0, 0): # pragma: no-cover-if-gtk4
return self.native.get_property("active")
else: # pragma: no-cover-if-gtk3
return self.native.get_property("spinning")

def rehint(self):
if GTK_VERSION < (4, 0, 0): # pragma: no-cover-if-gtk4
# print(
# "REHINT",
# self,
# self.native.get_preferred_width(),
# self.native.get_preferred_height(),
# )
width = self.native.get_preferred_width()[0]
height = self.native.get_preferred_height()[0]
else: # pragma: no-cover-if-gtk3
size = self.native.get_preferred_size()[0]
width, height = size.width, size.height
self.interface.intrinsic.width = width
self.interface.intrinsic.height = height
42 changes: 30 additions & 12 deletions gtk/src/toga_gtk/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from toga.window import _initial_position

from .container import TogaContainer
from .libs import GTK_VERSION, IS_WAYLAND, Gdk, GLib, Gtk
from .libs import GTK_VERSION, IS_WAYLAND, Adw, Gdk, GLib, Gtk

if GTK_VERSION >= (4, 0, 0): # pragma: no-cover-if-gtk3
from toga.handlers import WeakrefCallable
Expand Down Expand Up @@ -81,28 +81,43 @@ def __init__(self, interface, title, position, size):
# Window Decorator when resizable == False
self.native.set_resizable(self.interface.resizable)

# The GTK window's content is the layout; any user content is placed
# into the container, which is the bottom widget in the layout. The
# toolbar (if required) will be added at the top of the layout.
self.layout = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)

# Because expand and fill are True, the container will fill the available
# space, and will get a size_allocate callback if the window is resized.
self.container = TogaContainer()
if GTK_VERSION < (4, 0, 0): # pragma: no-cover-if-gtk4
# The GTK window's content is the layout; any user content is placed
# into the container, which is the bottom widget in the layout. The
# toolbar (if required) will be added at the top of the layout.
self.layout = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self.layout.pack_end(self.container, expand=True, fill=True, padding=0)
self.native.add(self.layout)
else: # pragma: no-cover-if-gtk3
self.container.set_valign(Gtk.Align.FILL)
self.container.set_vexpand(True)
self.layout.append(self.container)
self.native.set_child(self.layout)
if Adw is not None: # pragma: cover-if-libadwaita
toolbarview = Adw.ToolbarView()
self.headerbar = Adw.HeaderBar()
toolbarview.add_top_bar(self.headerbar)
self.container.set_valign(Gtk.Align.FILL)
self.container.set_vexpand(True)
toolbarview.set_content(self.container)
self.native.set_content(toolbarview)
else: # pragma: cover-if-plain-gtk4
# The GTK window's content is the layout; any user content is placed
# into the container, which is the bottom widget in the layout. The
# toolbar (if required) will be added at the top of the layout.
self.layout = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self.container.set_valign(Gtk.Align.FILL)
self.container.set_vexpand(True)
self.layout.append(self.container)
self.native.set_child(self.layout)

def create(self):
if GTK_VERSION < (4, 0, 0): # pragma: no-cover-if-gtk4
self.native = Gtk.Window()
else: # pragma: no-cover-if-gtk3
self.native = create_toga_native(Gtk.Window)()
if Adw is not None:
self.native = create_toga_native(Adw.Window)()
else:
self.native = create_toga_native(Gtk.Window)()

######################################################################
# Native event handlers
Expand Down Expand Up @@ -492,7 +507,10 @@ def create(self):
self.toolbar_items = {}
self.toolbar_separators = set()
else: # pragma: no-cover-if-gtk3
self.native = create_toga_native(Gtk.ApplicationWindow)()
if Adw is not None:
self.native = create_toga_native(Adw.ApplicationWindow)()
else:
self.native = create_toga_native(Gtk.ApplicationWindow)()

def create_menus(self):
# GTK menus are handled at the app level
Expand Down
7 changes: 5 additions & 2 deletions gtk/tests_backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import toga
from toga_gtk.keys import gtk_accel, toga_key
from toga_gtk.libs import GTK_VERSION, IS_WAYLAND, Gdk, Gtk
from toga_gtk.libs import GTK_VERSION, IS_WAYLAND, Adw, Gdk, Gtk

from .dialogs import DialogsMixin
from .probe import BaseProbe
Expand All @@ -27,7 +27,10 @@ class AppProbe(BaseProbe, DialogsMixin):
def __init__(self, app):
super().__init__()
self.app = app
assert isinstance(self.app._impl.native, Gtk.Application)
if Adw is None:
assert isinstance(self.app._impl.native, Gtk.Application)
else:
assert isinstance(self.app._impl.native, Adw.Application)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Technically, Adw.Application subclasses Gtk.Application... however, I used this stronger assertion because without Adw.Application, window bottom corners doesn't round at all.

Same with Adw.Window

assert IS_WAYLAND is (os.environ.get("WAYLAND_DISPLAY", "") != "")

@property
Expand Down
Loading