ags(control-center/pages): better page widget, add PageButton widget for better looking buttons

This commit is contained in:
retrozinndev
2025-04-21 18:06:19 -03:00
parent 83c62d120c
commit 151744defb
4 changed files with 316 additions and 277 deletions
+48 -40
View File
@@ -1,23 +1,24 @@
import { bind, Variable } from "astal"; import { bind } from "astal";
import { Gtk, Widget } from "astal/gtk3"; import { Gtk, Widget } from "astal/gtk3";
import AstalBluetooth from "gi://AstalBluetooth"; import AstalBluetooth from "gi://AstalBluetooth";
import { Page } from "./Page"; import { Page, PageButton } from "./Page";
import { Separator, SeparatorProps } from "../../Separator"; import { Separator, SeparatorProps } from "../../Separator";
const watchingDevices = new Variable<boolean>(false); export const BluetoothPage: (() => Page) = () => new Page({
id: "bluetooth",
export const BluetoothPage: Page = new Page({ title: "Bluetooth",
title: "Bluetooth Devices",
description: "Manage your Bluetooth devices and add new ones.", description: "Manage your Bluetooth devices and add new ones.",
className: "bluetooth", className: "bluetooth",
headerButtons: () => [ headerButtons: [
new Widget.Button({ new Widget.Button({
className: "discover nf", className: "discover nf",
label: watchingDevices(watching => !watching ? '󰑓' : '󰙦'), label: bind(AstalBluetooth.get_default().adapter, "discovering").as((discovering) =>
tooltipText: watchingDevices(watching => !watching ? "Start discovering" : "Stop discovery"), !discovering ? '󰑓' : '󰙦'),
tooltipText: bind(AstalBluetooth.get_default().adapter, "discovering").as((discovering) =>
!discovering ? "Start discovering" : "Stop discovery"),
onClick: () => { onClick: () => {
if(watchingDevices.get()) { if(AstalBluetooth.get_default().adapter.discovering) {
stopBluetoothDevicesWatch(); stopBluetoothDevicesWatch();
return; return;
} }
@@ -27,10 +28,29 @@ export const BluetoothPage: Page = new Page({
} as Widget.ButtonProps) } as Widget.ButtonProps)
], ],
onClose: () => stopBluetoothDevicesWatch(), onClose: () => stopBluetoothDevicesWatch(),
pageChild: () => new Widget.Box({ children: [
new Widget.Box({
className: "adapters",
visible: bind(AstalBluetooth.get_default(), "adapters").as((adapters) =>
adapters.length > 1),
children: bind(AstalBluetooth.get_default(), "adapters").as((adapters) => [
new Widget.Label({
className: "sub-header",
label: "Adapters"
} as Widget.LabelProps),
...adapters.map(adapter =>
PageButton({
title: adapter.alias ?? "Adapter",
icon: "bluetooth-active-symbolic",
onClick: () => AstalBluetooth.get_default(),
})
)
]
)
} as Widget.BoxProps),
new Widget.Box({
className: "connections", className: "connections",
orientation: Gtk.Orientation.VERTICAL, orientation: Gtk.Orientation.VERTICAL,
expand: true,
hexpand: true, hexpand: true,
children: [ children: [
new Widget.Box({ new Widget.Box({
@@ -81,10 +101,14 @@ export const BluetoothPage: Page = new Page({
} as Widget.ButtonProps) } as Widget.ButtonProps)
] ]
} as Widget.BoxProps) } as Widget.BoxProps)
]
}); });
function DeviceWidget(dev: AstalBluetooth.Device): Gtk.Widget { function DeviceWidget(dev: AstalBluetooth.Device): Gtk.Widget {
return new Widget.Button({ return PageButton({
className: bind(dev, "connected").as((connected) => connected ? "connected" : ""),
title: bind(dev, "alias").as(alias => alias ?? "Unknown Device"),
icon: dev.icon ?? "bluetooth-active-symbolic",
onClick: () => { onClick: () => {
if(dev.paired) { if(dev.paired) {
dev.connected ? dev.connected ?
@@ -99,46 +123,30 @@ function DeviceWidget(dev: AstalBluetooth.Device): Gtk.Widget {
dev.disconnect_device(null) dev.disconnect_device(null)
: dev.connect_device(null); : dev.connect_device(null);
}, },
className: bind(dev, "connected").as((connected) => connected ? "connected" : ""), endWidget: new Widget.Box({
child: new Widget.Box({
className: "device",
orientation: Gtk.Orientation.HORIZONTAL,
expand: true,
children: [
new Widget.Icon({
className: "icon",
icon: bind(dev, "icon").as((icon: string) =>
icon ? icon : "bluetooth-active-symbolic"),
css: "font-size: 20px; margin-right: 6px;"
} as Widget.IconProps),
new Widget.Label({
className: "alias",
halign: Gtk.Align.START,
hexpand: true,
label: bind(dev, "alias").as((alias) => alias.split('-').length === 6 ?
`Unknown (${alias})` : alias)
} as Widget.LabelProps),
new Widget.Label({
className: "battery",
halign: Gtk.Align.END,
visible: bind(dev, "batteryPercentage").as((bat: number) => visible: bind(dev, "batteryPercentage").as((bat: number) =>
bat <= -1 ? false : true), bat <= -1 ? false : true),
children: [
new Widget.Label({
halign: Gtk.Align.END,
label: bind(dev, "batteryPercentage").as((bat: number) => label: bind(dev, "batteryPercentage").as((bat: number) =>
`󰁹 ${Math.floor(bat * 100)}%`) `${Math.floor(bat * 100)}%`)
} as Widget.LabelProps) } as Widget.LabelProps),
new Widget.Icon({
icon: "battery-symbolic",
css: "font-size: 18px; margin-left: 6px;"
} as Widget.IconProps)
] ]
} as Widget.BoxProps) } as Widget.BoxProps)
} as Widget.ButtonProps) });
} }
function watchNewDevices(): void { function watchNewDevices(): void {
watchingDevices.set(true);
!AstalBluetooth.get_default().adapter.discovering && !AstalBluetooth.get_default().adapter.discovering &&
AstalBluetooth.get_default().adapter.start_discovery(); AstalBluetooth.get_default().adapter.start_discovery();
} }
export function stopBluetoothDevicesWatch(): void { export function stopBluetoothDevicesWatch(): void {
watchingDevices.set(false);
AstalBluetooth.get_default().adapter.discovering && AstalBluetooth.get_default().adapter.discovering &&
AstalBluetooth.get_default().adapter.stop_discovery(); AstalBluetooth.get_default().adapter.stop_discovery();
} }
+15 -32
View File
@@ -1,5 +1,5 @@
import { Gtk, Widget } from "astal/gtk3"; import { Gtk, Widget } from "astal/gtk3";
import { Page } from "./Page"; import { Page, PageButton } from "./Page";
import AstalNetwork from "gi://AstalNetwork"; import AstalNetwork from "gi://AstalNetwork";
import { bind } from "astal"; import { bind } from "astal";
import NM from "gi://NM"; import NM from "gi://NM";
@@ -7,10 +7,11 @@ import { Separator, SeparatorProps } from "../../Separator";
import { Windows } from "../../../windows"; import { Windows } from "../../../windows";
import AstalHyprland from "gi://AstalHyprland?version=0.1"; import AstalHyprland from "gi://AstalHyprland?version=0.1";
export const PageNetwork = new Page({ export const PageNetwork: (() => Page) = () => new Page({
id: "network",
title: "Network", title: "Network",
className: "network", className: "network",
headerButtons: () => [ headerButtons: [
new Widget.Button({ new Widget.Button({
className: "reload nf", className: "reload nf",
label: "󰑓", label: "󰑓",
@@ -21,9 +22,6 @@ export const PageNetwork = new Page({
onClick: () => AstalNetwork.get_default().wifi.scan() onClick: () => AstalNetwork.get_default().wifi.scan()
} as Widget.ButtonProps) } as Widget.ButtonProps)
], ],
pageChild: () => new Widget.Box({
expand: true,
orientation: Gtk.Orientation.VERTICAL,
children: [ children: [
new Widget.Box({ new Widget.Box({
className: "devices", className: "devices",
@@ -39,43 +37,29 @@ export const PageNetwork = new Page({
xalign: 0, xalign: 0,
className: "sub-header", className: "sub-header",
} as Widget.LabelProps), } as Widget.LabelProps),
...devices.filter(device => device.real).map(dev => new Widget.Button({ ...devices.filter(device => device.real).map(dev => PageButton({
className: "device", className: "device",
child: bind(AstalNetwork.get_default(), "client").as((client) => new Widget.Box({
children: [
new Widget.Icon({
className: "icon",
icon: bind(dev, "deviceType").as(deviceType => icon: bind(dev, "deviceType").as(deviceType =>
deviceType === NM.DeviceType.WIFI ? deviceType === NM.DeviceType.WIFI ?
"network-wireless-symbolic" "network-wireless-symbolic"
: "network-wired-symbolic"), : "network-wired-symbolic"),
css: "font-size: 20px; margin-right: 6px;" title: bind(dev, "interface").as(iface => iface ?? "Interface"),
} as Widget.IconProps), extraButtons: [
new Widget.Label({ new Widget.Button({
className: "interface name", image: new Widget.Icon({
xalign: 0,
hexpand: true,
label: bind(dev, "interface").as(iface => iface ?? "Unknown Interface")
} as Widget.LabelProps),
new Widget.Icon({
icon: "object-select-symbolic",
halign: Gtk.Align.END,
visible: bind(client, "primaryConnection").as((primaryConn) =>
primaryConn.devices.filter(device => device === dev)?.[0]).as(Boolean)
} as Widget.IconProps),
new Widget.EventBox({
child: new Widget.Icon({
icon: "view-more-symbolic" icon: "view-more-symbolic"
} as Widget.IconProps), } as Widget.IconProps),
onClick: () => { onClick: () => {
Windows.close("control-center"); Windows.close("control-center");
AstalHyprland.get_default().dispatch("exec", AstalHyprland.get_default().dispatch("exec",
`[animationstyle gnomed] nm-connection-editor --edit ${dev.get_udi()}`); `[animationstyle gnomed; float] nm-connection-editor --edit ${
dev.activeConnection?.connection.get_uuid()
}`);
} }
} as Widget.EventBoxProps) } as Widget.ButtonProps)
] ]
} as Widget.BoxProps)) })
} as Widget.ButtonProps)) )
] ]
}) })
} as Widget.BoxProps), } as Widget.BoxProps),
@@ -122,6 +106,5 @@ export const PageNetwork = new Page({
} }
} as Widget.ButtonProps) } as Widget.ButtonProps)
] ]
} as Widget.BoxProps)
}); });
@@ -1,17 +1,14 @@
import { Gtk, Widget } from "astal/gtk3"; import { Widget } from "astal/gtk3";
import { Page, PageProps } from "./Page"; import { Page, PageProps } from "./Page";
import { bind } from "astal"; import { bind } from "astal";
import { NightLight } from "../../../scripts/nightlight"; import { NightLight } from "../../../scripts/nightlight";
import { addSliderMarksFromMinMax } from "../../../scripts/widget-utils"; import { addSliderMarksFromMinMax } from "../../../scripts/widget-utils";
export const PageNightLight = new Page({ export const PageNightLight: (() => Page) = () => new Page({
id: "night-light",
title: "Night Light", title: "Night Light",
description: "Control night light and gamma filters", description: "Control night light and gamma filters",
pageChild: () => new Widget.Box({
hexpand: true,
className: "night-light", className: "night-light",
orientation: Gtk.Orientation.VERTICAL,
children: [ children: [
new Widget.Label({ new Widget.Label({
className: "sub-header", className: "sub-header",
@@ -50,5 +47,4 @@ export const PageNightLight = new Page({
NightLight.getDefault().gamma = (Math.floor(slider.value)), NightLight.getDefault().gamma = (Math.floor(slider.value)),
} as Widget.SliderProps) } as Widget.SliderProps)
] ]
} as Widget.BoxProps)
} as PageProps); } as PageProps);
+93 -41
View File
@@ -1,41 +1,39 @@
import { Binding, GObject, register } from "astal"; import { Binding, register } from "astal";
import { Gtk, Widget } from "astal/gtk3"; import { Gtk, Widget } from "astal/gtk3";
export type PageProps = { export type PageProps = {
setup?: () => void; setup?: () => void;
onClose?: () => void; onClose?: () => void;
onOpen?: () => void; onOpen?: () => void;
className?: string | Binding<string | undefined>; id: string;
title: string | Binding<string | undefined>; className?: string | Binding<string>;
description?: string | Binding<string | undefined>; title: string | Binding<string>;
headerButtons?: () => Array<Gtk.Widget>; description?: string | Binding<string>;
pageChild: () => Gtk.Widget; headerButtons?: Array<Gtk.Button> | Binding<Array<Gtk.Button>>;
orientation?: Gtk.Orientation | Binding<Gtk.Orientation>;
child?: Gtk.Widget | Binding<Gtk.Widget>;
children?: Array<Gtk.Widget> | Binding<Array<Gtk.Widget>>;
}; };
@register({ GTypeName: "Page" }) export { Page };
class Page extends GObject.Object {
readonly #props: PageProps;
get props() { return this.#props; } @register({ GTypeName: "Page" })
class Page extends Widget.Box {
readonly #id: string;
#title: string | Binding<string>;
#description: string | undefined | Binding<string>;
public get title() { return this.#title; }
public get description() { return this.#description; }
public get id() { return this.#id; }
constructor(props: PageProps) { constructor(props: PageProps) {
super(); super({
this.#props = props;
}
public getHeaderButtons(): (Array<Gtk.Widget>|null) {
return this.props.headerButtons ?
this.props.headerButtons()
: null;
}
public getPage(): Gtk.Widget {
return new Widget.Box({
className: (this.props.className instanceof Binding) ?
this.props.className.as((clsName: (string|undefined)) => `page ${ clsName || "" }`) : `page ${this.#props.className || ""}`,
orientation: Gtk.Orientation.VERTICAL,
hexpand: true, hexpand: true,
setup: this.props.setup, orientation: Gtk.Orientation.VERTICAL,
className: (props.className instanceof Binding) ?
props.className.as((clsName) => `page ${ clsName ?? "" }`)
: `page ${props.className ?? ""}`,
children: [ children: [
new Widget.Box({ new Widget.Box({
className: "header", className: "header",
@@ -43,22 +41,24 @@ class Page extends GObject.Object {
hexpand: true, hexpand: true,
children: [ children: [
new Widget.Box({ new Widget.Box({
className: "title", className: "top",
children: [ children: [
new Widget.Label({ new Widget.Label({
hexpand: true, hexpand: true,
className: "title", className: "title",
truncate: true, truncate: true,
visible: (this.props.title instanceof Binding) ? visible: (props.title instanceof Binding) ?
this.props.title.as(Boolean) props.title.as(Boolean)
: (this.props.title ? true : false), : (props.title ? true : false),
label: this.props.title, label: props.title,
halign: Gtk.Align.START halign: Gtk.Align.START
} as Widget.LabelProps), } as Widget.LabelProps),
new Widget.Box({ new Widget.Box({
className: "button-row", className: "button-row",
visible: Boolean(this.getHeaderButtons()), visible: (props.headerButtons instanceof Binding) ?
children: this.getHeaderButtons() || undefined props.headerButtons.as(Boolean)
: (props.headerButtons ? true : false),
children: props.headerButtons
} as Widget.BoxProps) } as Widget.BoxProps)
] ]
} as Widget.BoxProps), } as Widget.BoxProps),
@@ -67,22 +67,74 @@ class Page extends GObject.Object {
hexpand: true, hexpand: true,
truncate: true, truncate: true,
xalign: 0, xalign: 0,
visible: (this.props.description instanceof Binding) ? visible: (props.description instanceof Binding) ?
this.props.description.as(Boolean) props.description.as(Boolean)
: this.props.description ? true : false, : props.description ? true : false,
label: this.props.description label: props.description
} as Widget.LabelProps), } as Widget.LabelProps),
] ]
} as Widget.BoxProps), } as Widget.BoxProps),
new Widget.Box({ new Widget.Box({
className: "content", className: "content",
orientation: Gtk.Orientation.VERTICAL, orientation: props.orientation ?? Gtk.Orientation.VERTICAL,
expand: true, expand: true,
setup: (_) => _.add(this.props.pageChild()) setup: props.setup,
child: props.child,
children: props.children
} as Widget.BoxProps) } as Widget.BoxProps)
] ]
} as Widget.BoxProps); });
this.#id = props.id;
this.#title = props.title;
this.#description = props.description;
} }
} }
export { Page }; export function PageButton(props: {
className?: string | Binding<string>;
icon?: string | Binding<string>;
title: string | Binding<string>;
endWidget?: Gtk.Widget;
extraButtons?: Array<Widget.Button>;
onClick?: (self: Widget.Button) => void;
}): Gtk.Widget {
return new Widget.Box({
setup: (self) => {
self.add(new Widget.Button({
onClick: props.onClick,
className: props.className,
hexpand: true,
child: new Widget.Box({
className: "page-button",
orientation: Gtk.Orientation.HORIZONTAL,
expand: true,
setup: (box) => {
box.set_children([
new Widget.Icon({
className: "icon",
icon: props.icon,
visible: props.icon,
css: "font-size: 20px; margin-right: 6px;"
} as Widget.IconProps),
new Widget.Label({
className: "title",
halign: Gtk.Align.START,
hexpand: true,
truncate: true,
label: props.title
} as Widget.LabelProps)
]);
props.endWidget && box.add(props.endWidget);
}
} as Widget.BoxProps)
} as Widget.ButtonProps));
props.extraButtons && self.add(new Widget.Box({
className: "button-row extra-buttons",
children: props.extraButtons
} as Widget.BoxProps));
}
} as Widget.BoxProps);
}