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
+97 -89
View File
@@ -1,23 +1,24 @@
import { bind, Variable } from "astal";
import { bind } from "astal";
import { Gtk, Widget } from "astal/gtk3";
import AstalBluetooth from "gi://AstalBluetooth";
import { Page } from "./Page";
import { Page, PageButton } from "./Page";
import { Separator, SeparatorProps } from "../../Separator";
const watchingDevices = new Variable<boolean>(false);
export const BluetoothPage: Page = new Page({
title: "Bluetooth Devices",
export const BluetoothPage: (() => Page) = () => new Page({
id: "bluetooth",
title: "Bluetooth",
description: "Manage your Bluetooth devices and add new ones.",
className: "bluetooth",
headerButtons: () => [
headerButtons: [
new Widget.Button({
className: "discover nf",
label: watchingDevices(watching => !watching ? '󰑓' : '󰙦'),
tooltipText: watchingDevices(watching => !watching ? "Start discovering" : "Stop discovery"),
label: bind(AstalBluetooth.get_default().adapter, "discovering").as((discovering) =>
!discovering ? '󰑓' : '󰙦'),
tooltipText: bind(AstalBluetooth.get_default().adapter, "discovering").as((discovering) =>
!discovering ? "Start discovering" : "Stop discovery"),
onClick: () => {
if(watchingDevices.get()) {
if(AstalBluetooth.get_default().adapter.discovering) {
stopBluetoothDevicesWatch();
return;
}
@@ -27,64 +28,87 @@ export const BluetoothPage: Page = new Page({
} as Widget.ButtonProps)
],
onClose: () => stopBluetoothDevicesWatch(),
pageChild: () => new Widget.Box({
className: "connections",
orientation: Gtk.Orientation.VERTICAL,
expand: true,
hexpand: true,
children: [
new Widget.Box({
className: "paired",
orientation: Gtk.Orientation.VERTICAL,
visible: bind(AstalBluetooth.get_default(), "devices").as((devs) =>
devs.filter(dev => dev.paired || dev.connected).length > 0),
children: bind(AstalBluetooth.get_default(), "devices").as((devs: Array<AstalBluetooth.Device>) => {
const connectedDevices = devs.filter((dev: AstalBluetooth.Device) => dev.connected || dev.paired)
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",
orientation: Gtk.Orientation.VERTICAL,
hexpand: true,
children: [
new Widget.Box({
className: "paired",
orientation: Gtk.Orientation.VERTICAL,
visible: bind(AstalBluetooth.get_default(), "devices").as((devs) =>
devs.filter(dev => dev.paired || dev.connected).length > 0),
children: bind(AstalBluetooth.get_default(), "devices").as((devs: Array<AstalBluetooth.Device>) => {
const connectedDevices = devs.filter((dev: AstalBluetooth.Device) => dev.connected || dev.paired)
return [
new Widget.Label({
className: "sub-header",
label: "Paired Devices",
xalign: 0,
} as Widget.LabelProps),
...connectedDevices.map((dev: AstalBluetooth.Device) => DeviceWidget(dev))
]
})
} as Widget.BoxProps),
new Widget.Box({
className: "discovered",
orientation: Gtk.Orientation.VERTICAL,
visible: bind(AstalBluetooth.get_default(), "devices").as((devs) =>
devs.filter((dev) => !dev.connected && !dev.paired).length > 0),
children: bind(AstalBluetooth.get_default(), "devices").as((devices: Array<AstalBluetooth.Device>) => {
const discoveredDevices = devices.filter((dev: AstalBluetooth.Device) => !dev.connected && !dev.paired);
return [
new Widget.Label({
className: "sub-header",
label: "Paired Devices",
xalign: 0,
} as Widget.LabelProps),
...connectedDevices.map((dev: AstalBluetooth.Device) => DeviceWidget(dev))
]
})
} as Widget.BoxProps),
new Widget.Box({
className: "discovered",
orientation: Gtk.Orientation.VERTICAL,
visible: bind(AstalBluetooth.get_default(), "devices").as((devs) =>
devs.filter((dev) => !dev.connected && !dev.paired).length > 0),
children: bind(AstalBluetooth.get_default(), "devices").as((devices: Array<AstalBluetooth.Device>) => {
const discoveredDevices = devices.filter((dev: AstalBluetooth.Device) => !dev.connected && !dev.paired);
return [
new Widget.Label({
className: "sub-header",
label: "Others",
xalign: 0
} as Widget.LabelProps),
...discoveredDevices.map((dev: AstalBluetooth.Device) => DeviceWidget(dev))
]
})
} as Widget.BoxProps),
Separator({
size: .2,
orientation: Gtk.Orientation.VERTICAL,
alpha: .2
} as SeparatorProps),
new Widget.Button({
className: "more",
label: "More settings",
xalign: 0
} as Widget.ButtonProps)
]
} as Widget.BoxProps)
return [
new Widget.Label({
className: "sub-header",
label: "Others",
xalign: 0
} as Widget.LabelProps),
...discoveredDevices.map((dev: AstalBluetooth.Device) => DeviceWidget(dev))
]
})
} as Widget.BoxProps),
Separator({
size: .2,
orientation: Gtk.Orientation.VERTICAL,
alpha: .2
} as SeparatorProps),
new Widget.Button({
className: "more",
label: "More settings",
xalign: 0
} as Widget.ButtonProps)
]
} as Widget.BoxProps)
]
});
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: () => {
if(dev.paired) {
dev.connected ?
@@ -99,46 +123,30 @@ function DeviceWidget(dev: AstalBluetooth.Device): Gtk.Widget {
dev.disconnect_device(null)
: dev.connect_device(null);
},
className: bind(dev, "connected").as((connected) => connected ? "connected" : ""),
child: new Widget.Box({
className: "device",
orientation: Gtk.Orientation.HORIZONTAL,
expand: true,
endWidget: new Widget.Box({
visible: bind(dev, "batteryPercentage").as((bat: number) =>
bat <= -1 ? false : 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) =>
bat <= -1 ? false : true),
label: bind(dev, "batteryPercentage").as((bat: number) =>
`󰁹 ${Math.floor(bat * 100)}%`)
} as Widget.LabelProps)
`${Math.floor(bat * 100)}%`)
} as Widget.LabelProps),
new Widget.Icon({
icon: "battery-symbolic",
css: "font-size: 18px; margin-left: 6px;"
} as Widget.IconProps)
]
} as Widget.BoxProps)
} as Widget.ButtonProps)
});
}
function watchNewDevices(): void {
watchingDevices.set(true);
!AstalBluetooth.get_default().adapter.discovering &&
AstalBluetooth.get_default().adapter.start_discovery();
}
export function stopBluetoothDevicesWatch(): void {
watchingDevices.set(false);
AstalBluetooth.get_default().adapter.discovering &&
AstalBluetooth.get_default().adapter.stop_discovery();
}
+84 -101
View File
@@ -1,5 +1,5 @@
import { Gtk, Widget } from "astal/gtk3";
import { Page } from "./Page";
import { Page, PageButton } from "./Page";
import AstalNetwork from "gi://AstalNetwork";
import { bind } from "astal";
import NM from "gi://NM";
@@ -7,10 +7,11 @@ import { Separator, SeparatorProps } from "../../Separator";
import { Windows } from "../../../windows";
import AstalHyprland from "gi://AstalHyprland?version=0.1";
export const PageNetwork = new Page({
export const PageNetwork: (() => Page) = () => new Page({
id: "network",
title: "Network",
className: "network",
headerButtons: () => [
headerButtons: [
new Widget.Button({
className: "reload nf",
label: "󰑓",
@@ -21,107 +22,89 @@ export const PageNetwork = new Page({
onClick: () => AstalNetwork.get_default().wifi.scan()
} as Widget.ButtonProps)
],
pageChild: () => new Widget.Box({
expand: true,
orientation: Gtk.Orientation.VERTICAL,
children: [
new Widget.Box({
className: "devices",
hexpand: true,
orientation: Gtk.Orientation.VERTICAL,
visible: bind(AstalNetwork.get_default().get_client(), "devices").as((devs) => devs.length > 0),
children: bind(AstalNetwork.get_default().get_client(), "devices").as((devices) => {
devices = devices.filter(dev => dev.interface !== "lo");
children: [
new Widget.Box({
className: "devices",
hexpand: true,
orientation: Gtk.Orientation.VERTICAL,
visible: bind(AstalNetwork.get_default().get_client(), "devices").as((devs) => devs.length > 0),
children: bind(AstalNetwork.get_default().get_client(), "devices").as((devices) => {
devices = devices.filter(dev => dev.interface !== "lo");
return [
new Widget.Label({
label: "Devices",
xalign: 0,
className: "sub-header",
} as Widget.LabelProps),
...devices.filter(device => device.real).map(dev => new Widget.Button({
return [
new Widget.Label({
label: "Devices",
xalign: 0,
className: "sub-header",
} as Widget.LabelProps),
...devices.filter(device => device.real).map(dev => PageButton({
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 =>
deviceType === NM.DeviceType.WIFI ?
"network-wireless-symbolic"
: "network-wired-symbolic"),
css: "font-size: 20px; margin-right: 6px;"
icon: bind(dev, "deviceType").as(deviceType =>
deviceType === NM.DeviceType.WIFI ?
"network-wireless-symbolic"
: "network-wired-symbolic"),
title: bind(dev, "interface").as(iface => iface ?? "Interface"),
extraButtons: [
new Widget.Button({
image: new Widget.Icon({
icon: "view-more-symbolic"
} as Widget.IconProps),
new Widget.Label({
className: "interface name",
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"
} as Widget.IconProps),
onClick: () => {
Windows.close("control-center");
AstalHyprland.get_default().dispatch("exec",
`[animationstyle gnomed] nm-connection-editor --edit ${dev.get_udi()}`);
}
} as Widget.EventBoxProps)
]
} as Widget.BoxProps))
} as Widget.ButtonProps))
]
})
} as Widget.BoxProps),
new Widget.Box({
className: "wireless-aps",
visible: bind(AstalNetwork.get_default(), "primary").as((primary) => primary === AstalNetwork.Primary.WIFI),
hexpand: true,
orientation: Gtk.Orientation.VERTICAL,
children: AstalNetwork.get_default().wifi ? bind(AstalNetwork.get_default().wifi.get_device(), "accessPoints").as((aps) =>
aps.map(ap => new Widget.Button({
hexpand: true,
onClick: () => console.log("connect to " + ap.get_ssid().toArray().toString()), // TODO I don't have a WiFi board :(
child: new Widget.Box({
hexpand: true,
children: [
new Widget.Icon({
halign: Gtk.Align.START,
className: "icon",
icon: "network-wireless-signal-excellent-symbolic"
} as Widget.IconProps),
new Widget.Label({
className: "ssid",
halign: Gtk.Align.START,
label: ap.ssid.toArray().toString()
} as Widget.LabelProps),
new Widget.Label({
className: "status",
} as Widget.LabelProps)
onClick: () => {
Windows.close("control-center");
AstalHyprland.get_default().dispatch("exec",
`[animationstyle gnomed; float] nm-connection-editor --edit ${
dev.activeConnection?.connection.get_uuid()
}`);
}
} as Widget.ButtonProps)
]
} as Widget.BoxProps)
} as Widget.ButtonProps))) : [],
} as Widget.BoxProps),
Separator({
orientation: Gtk.Orientation.VERTICAL,
alpha: .2,
size: .2
} as SeparatorProps),
new Widget.Button({
label: "More settings",
xalign: 0,
onClick: () => {
Windows.close("control-center");
AstalHyprland.get_default().dispatch("exec", "[animationstyle gnomed] nm-connection-editor");
}
} as Widget.ButtonProps)
]
} as Widget.BoxProps)
})
)
]
})
} as Widget.BoxProps),
new Widget.Box({
className: "wireless-aps",
visible: bind(AstalNetwork.get_default(), "primary").as((primary) => primary === AstalNetwork.Primary.WIFI),
hexpand: true,
orientation: Gtk.Orientation.VERTICAL,
children: AstalNetwork.get_default().wifi ? bind(AstalNetwork.get_default().wifi.get_device(), "accessPoints").as((aps) =>
aps.map(ap => new Widget.Button({
hexpand: true,
onClick: () => console.log("connect to " + ap.get_ssid().toArray().toString()), // TODO I don't have a WiFi board :(
child: new Widget.Box({
hexpand: true,
children: [
new Widget.Icon({
halign: Gtk.Align.START,
className: "icon",
icon: "network-wireless-signal-excellent-symbolic"
} as Widget.IconProps),
new Widget.Label({
className: "ssid",
halign: Gtk.Align.START,
label: ap.ssid.toArray().toString()
} as Widget.LabelProps),
new Widget.Label({
className: "status",
} as Widget.LabelProps)
]
} as Widget.BoxProps)
} as Widget.ButtonProps))) : [],
} as Widget.BoxProps),
Separator({
orientation: Gtk.Orientation.VERTICAL,
alpha: .2,
size: .2
} as SeparatorProps),
new Widget.Button({
label: "More settings",
xalign: 0,
onClick: () => {
Windows.close("control-center");
AstalHyprland.get_default().dispatch("exec", "[animationstyle gnomed] nm-connection-editor");
}
} as Widget.ButtonProps)
]
});
+42 -46
View File
@@ -1,54 +1,50 @@
import { Gtk, Widget } from "astal/gtk3";
import { Widget } from "astal/gtk3";
import { Page, PageProps } from "./Page";
import { bind } from "astal";
import { NightLight } from "../../../scripts/nightlight";
import { addSliderMarksFromMinMax } from "../../../scripts/widget-utils";
export const PageNightLight = new Page({
export const PageNightLight: (() => Page) = () => new Page({
id: "night-light",
title: "Night Light",
description: "Control night light and gamma filters",
pageChild: () => new Widget.Box({
hexpand: true,
className: "night-light",
orientation: Gtk.Orientation.VERTICAL,
children: [
new Widget.Label({
className: "sub-header",
label: "Temperature (blue-light filter)",
xalign: 0
} as Widget.LabelProps),
new Widget.Slider({
className: "temperature",
setup: (slider) => {
slider.value = NightLight.getDefault().temperature;
addSliderMarksFromMinMax(slider, 5, "{}K");
},
value: bind(NightLight.getDefault(), "temperature"),
tooltipText: bind(NightLight.getDefault(), "temperature").as((temp) => `${temp}K`),
min: 1000,
max: bind(NightLight.getDefault(), "maxTemperature"),
onDragged: (slider) =>
NightLight.getDefault().temperature = (Math.floor(slider.value)),
} as Widget.SliderProps),
new Widget.Label({
className: "sub-header",
label: "Gamma (light filter)",
css: "margin-top: 6px;",
xalign: 0
} as Widget.LabelProps),
new Widget.Slider({
className: "gamma",
setup: (slider) => {
slider.value = NightLight.getDefault().gamma;
addSliderMarksFromMinMax(slider, 5, "{}%");
},
value: bind(NightLight.getDefault(), "gamma"),
max: bind(NightLight.getDefault(), "maxGamma"),
tooltipText: bind(NightLight.getDefault(), "gamma").as((gamma) => `${gamma}%`),
onDragged: (slider) =>
NightLight.getDefault().gamma = (Math.floor(slider.value)),
} as Widget.SliderProps)
]
} as Widget.BoxProps)
className: "night-light",
children: [
new Widget.Label({
className: "sub-header",
label: "Temperature (blue-light filter)",
xalign: 0
} as Widget.LabelProps),
new Widget.Slider({
className: "temperature",
setup: (slider) => {
slider.value = NightLight.getDefault().temperature;
addSliderMarksFromMinMax(slider, 5, "{}K");
},
value: bind(NightLight.getDefault(), "temperature"),
tooltipText: bind(NightLight.getDefault(), "temperature").as((temp) => `${temp}K`),
min: 1000,
max: bind(NightLight.getDefault(), "maxTemperature"),
onDragged: (slider) =>
NightLight.getDefault().temperature = (Math.floor(slider.value)),
} as Widget.SliderProps),
new Widget.Label({
className: "sub-header",
label: "Gamma (light filter)",
css: "margin-top: 6px;",
xalign: 0
} as Widget.LabelProps),
new Widget.Slider({
className: "gamma",
setup: (slider) => {
slider.value = NightLight.getDefault().gamma;
addSliderMarksFromMinMax(slider, 5, "{}%");
},
value: bind(NightLight.getDefault(), "gamma"),
max: bind(NightLight.getDefault(), "maxGamma"),
tooltipText: bind(NightLight.getDefault(), "gamma").as((gamma) => `${gamma}%`),
onDragged: (slider) =>
NightLight.getDefault().gamma = (Math.floor(slider.value)),
} as Widget.SliderProps)
]
} 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";
export type PageProps = {
setup?: () => void;
onClose?: () => void;
onOpen?: () => void;
className?: string | Binding<string | undefined>;
title: string | Binding<string | undefined>;
description?: string | Binding<string | undefined>;
headerButtons?: () => Array<Gtk.Widget>;
pageChild: () => Gtk.Widget;
id: string;
className?: string | Binding<string>;
title: string | Binding<string>;
description?: string | Binding<string>;
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" })
class Page extends GObject.Object {
readonly #props: PageProps;
export { Page };
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) {
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,
super({
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: [
new Widget.Box({
className: "header",
@@ -43,22 +41,24 @@ class Page extends GObject.Object {
hexpand: true,
children: [
new Widget.Box({
className: "title",
className: "top",
children: [
new Widget.Label({
hexpand: true,
className: "title",
truncate: true,
visible: (this.props.title instanceof Binding) ?
this.props.title.as(Boolean)
: (this.props.title ? true : false),
label: this.props.title,
visible: (props.title instanceof Binding) ?
props.title.as(Boolean)
: (props.title ? true : false),
label: props.title,
halign: Gtk.Align.START
} as Widget.LabelProps),
new Widget.Box({
className: "button-row",
visible: Boolean(this.getHeaderButtons()),
children: this.getHeaderButtons() || undefined
visible: (props.headerButtons instanceof Binding) ?
props.headerButtons.as(Boolean)
: (props.headerButtons ? true : false),
children: props.headerButtons
} as Widget.BoxProps)
]
} as Widget.BoxProps),
@@ -67,22 +67,74 @@ class Page extends GObject.Object {
hexpand: true,
truncate: true,
xalign: 0,
visible: (this.props.description instanceof Binding) ?
this.props.description.as(Boolean)
: this.props.description ? true : false,
label: this.props.description
visible: (props.description instanceof Binding) ?
props.description.as(Boolean)
: props.description ? true : false,
label: props.description
} as Widget.LabelProps),
]
} as Widget.BoxProps),
new Widget.Box({
className: "content",
orientation: Gtk.Orientation.VERTICAL,
orientation: props.orientation ?? Gtk.Orientation.VERTICAL,
expand: true,
setup: (_) => _.add(this.props.pageChild())
setup: props.setup,
child: props.child,
children: props.children
} 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);
}