feat(control-center/tiles): update tiles look and structure

This commit is contained in:
retrozinndev
2025-08-18 10:56:52 -03:00
parent 74968ff17a
commit ddc5121163
8 changed files with 195 additions and 243 deletions
+40 -51
View File
@@ -199,78 +199,67 @@
.tiles-container { .tiles-container {
@include mixins.reset-props; @include mixins.reset-props;
& > flowbox { & .tile {
& > flowboxchild .tile {
$radius: 18px; $radius: 18px;
$padding: 4px;
&:not(.toggled) > .toggle-button, background: rgba(colors.$bg-primary, .5);
&:not(.toggled) > button.more {
background: colors.$bg-primary;
}
&:not(.toggled) > .toggle-button:hover,
&:not(.toggled) > button.more:hover {
background: color.scale($color: colors.$bg-primary, $lightness: 10%);
}
&.toggled .toggle-button:hover,
&.toggled button.more:hover {
background: colors.$bg-tertiary;
}
&.toggled > .toggle-button,
&.toggled > button.more {
background: colors.$bg-secondary;
}
&.has-more > .toggle-button,
&.has-more > button.toggle-button:active {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
& > button.toggle-button {
border-radius: $radius; border-radius: $radius;
padding: $padding;
min-height: 40px;
& .icon {
transition: 120ms ease-in;
border-radius: calc($radius - $padding);
padding: 8px 12px;
margin-right: 6px;
background: color.scale($color: colors.$bg-primary, $lightness: 10%);
& image {
-gtk-icon-size: 18px;
}
&:hover {
background: color.scale($color: colors.$bg-primary, $lightness: 15%);
}
&:active { &:active {
border-radius: calc($radius - 4px); border-radius: calc($radius - $padding - 2px);
}
} }
& .content { & .content {
padding: 8px; & .title {
& > .icon {
margin-right: 6px;
}
& > .text {
& > .title {
font-weight: 600; font-weight: 600;
font-size: 15.1px; font-size: 15.1px;
} }
& > .description { & .description {
font-size: 12px; font-size: 12px;
color: colors.$fg-disabled; color: colors.$fg-disabled;
font-weight: 400; font-weight: 400;
} }
} }
}
& .arrow {
-gtk-icon-size: 12px;
color: rgba(colors.$fg-disabled, .4);
} }
& > button.more { &:hover {
border-top-right-radius: $radius; background: color.scale($color: colors.$bg-primary, $lightness: 5%);
border-bottom-right-radius: $radius; }
&.enabled .icon {
background: colors.$bg-secondary;
&:hover {
background: colors.$bg-tertiary;
}
}
&:active { &:active {
border-top-right-radius: calc($radius - 4px); border-radius: calc($radius - 2px);
border-bottom-right-radius: calc($radius - 4px);
}
& label {
font-size: 16px;
}
}
} }
} }
} }
+37 -8
View File
@@ -10,6 +10,7 @@ import { generalConfig, Shell } from "../app";
import AstalIO from "gi://AstalIO"; import AstalIO from "gi://AstalIO";
import AstalMpris from "gi://AstalMpris"; import AstalMpris from "gi://AstalMpris";
import { Gtk } from "ags/gtk4";
export type RemoteCaller = { export type RemoteCaller = {
@@ -21,7 +22,7 @@ let wsTimeout: AstalIO.Time|undefined;
const help = `Manage Astal Windows and do more stuff. From retrozinndev's colorshell, \ const help = `Manage Astal Windows and do more stuff. From retrozinndev's colorshell, \
made using GTK4, AGS, Gnim and Astal libraries by Aylur. made using GTK4, AGS, Gnim and Astal libraries by Aylur.
Window Management: Window Management:
open [window]: opens the specified window. open [window]: opens the specified window.
close [window]: closes all instances of specified window. close [window]: closes all instances of specified window.
toggle [window]: toggle-open/close the specified window. toggle [window]: toggle-open/close the specified window.
@@ -30,21 +31,24 @@ made using GTK4, AGS, Gnim and Astal libraries by Aylur.
reopen: restart all open-windows. reopen: restart all open-windows.
quit: exit the main instance of the shell. quit: exit the main instance of the shell.
Audio Controls: Audio Controls:
volume: speaker and microphone volume controller, see "volume help". volume: speaker and microphone volume controller, see "volume help".
Media Controls: Media Controls:
media: manage colorshell's active player, see "media help". media: manage colorshell's active player, see "media help".
${DEVEL ? `
Other options: Development Tools:
dev: tools to help debugging colorshell
` : ""}
Other options:
runner [initial_text]: open the application runner, optionally add an initial search. runner [initial_text]: open the application runner, optionally add an initial search.
peek-workspace-num [millis]: peek the workspace numbers on bar window. peek-workspace-num [millis]: peek the workspace numbers on bar window.
v, version: display current colorshell version. v, version: display current colorshell version.
h, help: shows this help message. h, help: shows this help message.
2025 (c) retrozinndev's colorshell, licensed under the MIT License. 2025 (c) retrozinndev's colorshell, licensed under the MIT License.
https://github.com/retrozinndev/colorshell https://github.com/retrozinndev/colorshell
`.split('\n').map(l => l.replace(/^ {8}/, "")).join('\n'); `.trim();
export function handleArguments(cmd: RemoteCaller, args: Array<string>): number { export function handleArguments(cmd: RemoteCaller, args: Array<string>): number {
switch(args[0]) { switch(args[0]) {
@@ -59,6 +63,9 @@ export function handleArguments(cmd: RemoteCaller, args: Array<string>): number
}${DEVEL ? " (devel)" : ""}\nhttps://github.com/retrozinndev/colorshell`); }${DEVEL ? " (devel)" : ""}\nhttps://github.com/retrozinndev/colorshell`);
return 0; return 0;
case "dev":
return handleDevArgs(cmd, args);
case "open": case "open":
case "close": case "close":
case "toggle": case "toggle":
@@ -115,6 +122,28 @@ export function handleArguments(cmd: RemoteCaller, args: Array<string>): number
return 1; return 1;
} }
function handleDevArgs(cmd: RemoteCaller, args: Array<string>): number {
if(/h|help/.test(args[1])) {
cmd.print_literal(`
Debugging tools for colorshell.
Options:
inspector: open GTK's visual debugger
`.trim());
return 0;
}
switch(args[1]) {
case "inspector":
cmd.print_literal("Opening inspector...");
Gtk.Window.set_interactive_debugging(true);
return 0;
}
cmd.printerr_literal("Error: command not found! try checking `dev help`");
return 1;
}
function handleMediaArgs(cmd: RemoteCaller, args: Array<string>): number { function handleMediaArgs(cmd: RemoteCaller, args: Array<string>): number {
if(/h|help/.test(args[1])) { if(/h|help/.test(args[1])) {
const mediaHelp = ` const mediaHelp = `
@@ -11,11 +11,12 @@ export const TileBluetooth = () =>
} description={createBinding(AstalBluetooth.get_default(), "isConnected").as((connected) => { } description={createBinding(AstalBluetooth.get_default(), "isConnected").as((connected) => {
const connectedDev = AstalBluetooth.get_default().devices.filter(dev => dev.connected)?.[0]; const connectedDev = AstalBluetooth.get_default().devices.filter(dev => dev.connected)?.[0];
return connected && connectedDev ? connectedDev.get_alias() : "" return connected && connectedDev ? connectedDev.get_alias() : ""
})} onToggledOn={() => AstalBluetooth.get_default().adapter?.set_powered(true)} })}
onToggledOff={() => AstalBluetooth.get_default().adapter?.set_powered(false)} onEnabled={() => AstalBluetooth.get_default().adapter?.set_powered(true)}
onClickMore={() => TilesPages?.toggle(BluetoothPage)} onDisabled={() => AstalBluetooth.get_default().adapter?.set_powered(false)}
enableOnClickMore={true} iconSize={16} onClicked={() => TilesPages?.toggle(BluetoothPage)}
toggleState={createBinding(AstalBluetooth.get_default(), "isPowered")} enableOnClicked hasArrow
state={createBinding(AstalBluetooth.get_default(), "isPowered")}
icon={createComputed([ icon={createComputed([
createBinding(AstalBluetooth.get_default(), "isPowered"), createBinding(AstalBluetooth.get_default(), "isPowered"),
createBinding(AstalBluetooth.get_default(), "isConnected") createBinding(AstalBluetooth.get_default(), "isConnected")
@@ -24,5 +25,6 @@ export const TileBluetooth = () =>
powered ? ( isConnected ? powered ? ( isConnected ?
"bluetooth-active-symbolic" "bluetooth-active-symbolic"
: "bluetooth-symbolic" : "bluetooth-symbolic"
) : "bluetooth-disabled-symbolic")} ) : "bluetooth-disabled-symbolic")
}
/>; />;
@@ -7,9 +7,8 @@ export const TileDND = () =>
<Tile title={tr("control_center.tiles.dnd.title")} <Tile title={tr("control_center.tiles.dnd.title")}
description={createBinding(Notifications.getDefault().getNotifd(), "dontDisturb").as( description={createBinding(Notifications.getDefault().getNotifd(), "dontDisturb").as(
(dnd: boolean) => dnd ? tr("control_center.tiles.enabled") : tr("control_center.tiles.disabled"))} (dnd: boolean) => dnd ? tr("control_center.tiles.enabled") : tr("control_center.tiles.disabled"))}
onToggledOff={() => Notifications.getDefault().getNotifd().dontDisturb = false} onDisabled={() => Notifications.getDefault().getNotifd().dontDisturb = false}
onToggledOn={() => Notifications.getDefault().getNotifd().dontDisturb = true} onEnabled={() => Notifications.getDefault().getNotifd().dontDisturb = true}
icon={"minus-circle-filled-symbolic"} icon={"minus-circle-filled-symbolic"}
iconSize={16} state={Notifications.getDefault().getNotifd().dontDisturb}
toggleState={Notifications.getDefault().getNotifd().dontDisturb}
/>; />;
+12 -14
View File
@@ -29,11 +29,11 @@ export const TileNetwork = () => <Gtk.Box>
return tr("connecting") + "..."; return tr("connecting") + "...";
} }
})() })()
)} onToggledOn={() => wifi.set_enabled(true)} )} onEnabled={() => wifi.set_enabled(true)}
onToggledOff={() => wifi.set_enabled(false)} onDisabled={() => wifi.set_enabled(false)}
onClickMore={() => TilesPages?.toggle(PageNetwork)} hasArrow onClicked={() => TilesPages?.toggle(PageNetwork)}
icon={"network-wireless-signal-excellent-symbolic"} icon={"network-wireless-signal-excellent-symbolic"}
toggleState={createBinding(wifi, "enabled")} state={createBinding(wifi, "enabled")}
/> />
} else if(primary === AstalNetwork.Primary.WIRED) { } else if(primary === AstalNetwork.Primary.WIRED) {
@@ -48,9 +48,9 @@ export const TileNetwork = () => <Gtk.Box>
return tr("connecting") + "..."; return tr("connecting") + "...";
} }
})} })}
onToggledOn={() => execAsync("nmcli n on")} hasArrow onEnabled={() => execAsync("nmcli n on")}
onToggledOff={() => execAsync("nmcli n off")} onDisabled={() => execAsync("nmcli n off")}
onClickMore={() => TilesPages?.toggle(PageNetwork)} onClicked={() => TilesPages?.toggle(PageNetwork)}
icon={createBinding(wired, "internet").as((internet: AstalNetwork.Internet) => { icon={createBinding(wired, "internet").as((internet: AstalNetwork.Internet) => {
switch(internet) { switch(internet) {
case AstalNetwork.Internet.CONNECTED: case AstalNetwork.Internet.CONNECTED:
@@ -61,8 +61,7 @@ export const TileNetwork = () => <Gtk.Box>
return "network-wired-no-route-symbolic"; return "network-wired-no-route-symbolic";
})} })}
iconSize={16} state={createBinding(wired, "internet").as((internet: AstalNetwork.Internet) =>
toggleState={createBinding(wired, "internet").as((internet: AstalNetwork.Internet) =>
internet === AstalNetwork.Internet.CONNECTING internet === AstalNetwork.Internet.CONNECTING
|| internet === AstalNetwork.Internet.CONNECTED || internet === AstalNetwork.Internet.CONNECTED
)} )}
@@ -72,12 +71,11 @@ export const TileNetwork = () => <Gtk.Box>
return <Tile return <Tile
title={tr("control_center.tiles.network.network")} title={tr("control_center.tiles.network.network")}
description={tr("disconnected")} description={tr("disconnected")}
onToggledOn={() => execAsync("nmcli n on")} onEnabled={() => execAsync("nmcli n on")}
onToggledOff={() => execAsync("nmcli n off")} onDisabled={() => execAsync("nmcli n off")}
onClickMore={() => TilesPages?.toggle(PageNetwork)} hasArrow onClicked={() => TilesPages?.toggle(PageNetwork)}
icon={"network-wired-disconnected-symbolic"} icon={"network-wired-disconnected-symbolic"}
iconSize={16} state={createBinding(wired, "internet").as((internet: AstalNetwork.Internet) =>
toggleState={createBinding(wired, "internet").as((internet: AstalNetwork.Internet) =>
internet === AstalNetwork.Internet.CONNECTING || internet === AstalNetwork.Internet.CONNECTED)} internet === AstalNetwork.Internet.CONNECTING || internet === AstalNetwork.Internet.CONNECTED)}
/> />
}} }}
@@ -16,10 +16,10 @@ export const TileNightLight = () =>
tr("control_center.tiles.night_light.default_desc") : `${temp}K`} ${ tr("control_center.tiles.night_light.default_desc") : `${temp}K`} ${
gamma < NightLight.getDefault().maxGamma ? `(${gamma}%)` : ""}` gamma < NightLight.getDefault().maxGamma ? `(${gamma}%)` : ""}`
)} )}
visible={isInstalled("hyprsunset")} hasArrow visible={isInstalled("hyprsunset")}
onToggledOff={() => NightLight.getDefault().identity = true} onDisabled={() => NightLight.getDefault().identity = true}
onToggledOn={() => NightLight.getDefault().identity = false} onEnabled={() => NightLight.getDefault().identity = false}
enableOnClickMore={true} enableOnClicked
onClickMore={() => TilesPages?.toggle(PageNightLight)} onClicked={() => TilesPages?.toggle(PageNightLight)}
toggleState={createBinding(NightLight.getDefault(), "identity").as(identity => !identity)} state={createBinding(NightLight.getDefault(), "identity").as(identity => !identity)}
/> />
@@ -24,8 +24,7 @@ export const TileRecording = () =>
})} })}
icon={"media-record-symbolic"} icon={"media-record-symbolic"}
visible={isInstalled("wf-recorder")} visible={isInstalled("wf-recorder")}
onToggledOff={() => Recording.getDefault().stopRecording()} onDisabled={() => Recording.getDefault().stopRecording()}
onToggledOn={() => Recording.getDefault().startRecording()} onEnabled={() => Recording.getDefault().startRecording()}
toggleState={createBinding(Recording.getDefault(), "recording")} state={createBinding(Recording.getDefault(), "recording")}
iconSize={16}
/>; />;
+54 -118
View File
@@ -1,30 +1,13 @@
import { Gtk } from "ags/gtk4"; import { Gtk } from "ags/gtk4";
import { tr } from "../../../i18n/intl"; import { createBinding } from "ags";
import { Accessor, createBinding, createComputed, createState, getScope, onCleanup } from "ags";
import { omitObjectKeys, variableToBoolean } from "../../../modules/utils"; import { omitObjectKeys, variableToBoolean } from "../../../modules/utils";
import GObject, { property, register, signal } from "ags/gobject"; import { property, register, signal } from "ags/gobject";
import Pango from "gi://Pango?version=1.0"; import Pango from "gi://Pango?version=1.0";
export { Tile, TileProps }; export { Tile };
type TileProps = {
class?: string | Accessor<string>;
icon?: string | Accessor<string>;
visible?: boolean | Accessor<boolean>;
iconSize?: number | Accessor<number>;
title: string | Accessor<string>;
description?: string | Accessor<string>;
toggleState?: boolean | Accessor<boolean>;
enableOnClickMore?: boolean | Accessor<boolean>;
onUnmap?: () => void;
onToggledOn: () => void;
onToggledOff: () => void;
onClickMore?: () => void;
};
/* TODO: finish the tile class
@register({ GTypeName: "Tile" }) @register({ GTypeName: "Tile" })
class Tile extends Gtk.Box { class Tile extends Gtk.Box {
@signal(Boolean) toggled(_state: boolean) {} @signal(Boolean) toggled(_state: boolean) {}
@@ -42,6 +25,8 @@ class Tile extends Gtk.Box {
public enableOnClicked: boolean = true; public enableOnClicked: boolean = true;
@property(Boolean) @property(Boolean)
public state: boolean = false; public state: boolean = false;
@property(Boolean)
public hasArrow: boolean = false;
declare $signals: Gtk.Box.SignalSignatures & { declare $signals: Gtk.Box.SignalSignatures & {
"toggled": (_state: boolean) => void; "toggled": (_state: boolean) => void;
@@ -53,25 +38,29 @@ class Tile extends Gtk.Box {
public enable(): void { public enable(): void {
if(this.state) return; if(this.state) return;
this.state = true;
!this.has_css_class("enabled") &&
this.add_css_class("enabled");
this.emit("toggled", true); this.emit("toggled", true);
this.emit("enabled"); this.emit("enabled");
this.state = true;
} }
public disable(): void { public disable(): void {
if(!this.state) return; if(!this.state) return;
this.state = false;
this.remove_css_class("enabled");
this.emit("toggled", false); this.emit("toggled", false);
this.emit("disabled"); this.emit("disabled");
this.state = false;
} }
constructor(props: Omit<Gtk.Box.ConstructorProps, "orientation"> & { constructor(props: Partial<Omit<Gtk.Box.ConstructorProps, "orientation">> & {
icon: string; icon: string;
title: string; title: string;
description?: string; description?: string;
state?: boolean; state?: boolean;
enableOnClicked?: boolean; enableOnClicked?: boolean;
hasArrow?: boolean;
}) { }) {
super(omitObjectKeys(props, [ super(omitObjectKeys(props, [
"icon", "icon",
@@ -81,9 +70,14 @@ class Tile extends Gtk.Box {
"enableOnClicked" "enableOnClicked"
])); ]));
this.add_css_class("tile");
this.icon = props.icon; this.icon = props.icon;
this.title = props.title; this.title = props.title;
if(props.hasArrow != null)
this.hasArrow = props.hasArrow;
if(props.description != null) if(props.description != null)
this.description = props.description; this.description = props.description;
@@ -93,32 +87,52 @@ class Tile extends Gtk.Box {
if(props.enableOnClicked != null) if(props.enableOnClicked != null)
this.enableOnClicked = props.enableOnClicked; this.enableOnClicked = props.enableOnClicked;
const connections = new Map<GObject.Object, number>(); if(this.state)
const gestureClick = Gtk.GestureClick.new(); this.add_css_class("enabled"); // fix no highlight with state = true on construct
this.add_controller(gestureClick);
connections.set(gestureClick, gestureClick.connect("released", () => {
this.emit("clicked");
if(this.enableOnClicked && !this.state)
this.enable();
return true;
}));
this.prepend( this.prepend(
<Gtk.Box hexpand={false} vexpand> <Gtk.Box hexpand={false} vexpand class={"icon"}>
<Gtk.Image iconName={createBinding(this, "icon")} /> <Gtk.Image iconName={createBinding(this, "icon")} halign={Gtk.Align.CENTER} />
<Gtk.GestureClick onReleased={() => {
this.state ? this.disable() : this.enable();
}} />
</Gtk.Box> as Gtk.Box </Gtk.Box> as Gtk.Box
); );
this.append( this.append(
<Gtk.Box class={"content"} orientation={Gtk.Orientation.VERTICAL}> <Gtk.Box class={"content"} orientation={Gtk.Orientation.VERTICAL} vexpand
<Gtk.Label class={"title"} label={createBinding(this, "title")} /> valign={Gtk.Align.CENTER} hexpand>
<Gtk.Label class={"description"} label={createBinding(this, "description")} />
<Gtk.Label class={"title"} label={createBinding(this, "title")}
xalign={0} ellipsize={Pango.EllipsizeMode.END} />
<Gtk.Label class={"description"} label={createBinding(this, "description")}
xalign={0} ellipsize={Pango.EllipsizeMode.END} visible={
variableToBoolean(createBinding(this, "description"))
}
/>
<Gtk.GestureClick onReleased={() => {
this.emit("clicked");
if(this.enableOnClicked && !this.state)
this.enable();
return true;
}} />
</Gtk.Box> as Gtk.Box </Gtk.Box> as Gtk.Box
); );
getScope()?.onCleanup(() => connections.forEach((id, obj) => obj.disconnect(id))); if(this.hasArrow)
this.append(
<Gtk.Image class={"arrow"} iconName={"go-next-symbolic"}>
<Gtk.GestureClick onReleased={() => {
this.emit("clicked");
if(this.enableOnClicked && !this.state)
this.enable();
return true;
}} />
</Gtk.Image> as Gtk.Image
);
} }
emit<Signal extends keyof typeof this.$signals>( emit<Signal extends keyof typeof this.$signals>(
@@ -135,81 +149,3 @@ class Tile extends Gtk.Box {
return super.connect(signal, callback); return super.connect(signal, callback);
} }
} }
*/
function Tile(props: TileProps): Gtk.Widget {
const subs: Array<() => void> = [];
const [toggled, setToggled] = createState(((props.toggleState instanceof Accessor) ?
props.toggleState.get()
: props.toggleState) ?? false);
(props.toggleState instanceof Accessor) && subs.push(
props.toggleState.subscribe(() =>
setToggled((props.toggleState as Accessor<boolean>).get() ?? false))
);
onCleanup(() => subs.forEach(s => s()));
return <Gtk.Box hexpand visible={props.visible} onUnmap={props.onUnmap}
canFocus focusable={false} class={
(props.class instanceof Accessor) ?
createComputed([props.class, toggled], (clss, isToggled) =>
`tile ${clss} ${isToggled ? "toggled" : ""} ${
props.onClickMore ? "has-more" : ""
}`
)
: toggled.as(isToggled =>
`tile ${props.class ? props.class : ""} ${isToggled ? "toggled" : ""} ${
props.onClickMore ? "has-more" : ""
}`
)
}>
<Gtk.Button class={"toggle-button"} onClicked={() => {
if(toggled.get()) {
setToggled(false);
props.onToggledOff?.();
return;
}
setToggled(true);
props.onToggledOn?.();
}}>
<Gtk.Box class={"content"} hexpand={true} vexpand={true}>
{props.icon && <Gtk.Image class={"icon"} iconName={props.icon} css={
(props.iconSize instanceof Accessor) ?
props.iconSize.as(size => `font-size: ${size}px;`)
: (props.iconSize ?
`font-size: ${props.iconSize ?? 16}px;`
: undefined)
} />}
<Gtk.Box orientation={Gtk.Orientation.VERTICAL} class={"text"} vexpand={true} hexpand={true}
valign={Gtk.Align.CENTER}>
<Gtk.Label class={"title"} xalign={0} halign={Gtk.Align.START} ellipsize={Pango.EllipsizeMode.END}
label={props.title} />
{props.description && <Gtk.Label class={"description"} ellipsize={Pango.EllipsizeMode.END}
visible={variableToBoolean(props.description)} xalign={0} label={
(props.description instanceof Accessor) ?
props.description.as(str => str ?? "")
: (props.description ?? "")
} halign={Gtk.Align.START}
/>}
</Gtk.Box>
</Gtk.Box>
</Gtk.Button>
<Gtk.Button class={"more icon"} iconName={"go-next-symbolic"} widthRequest={32}
visible={Boolean(props.onClickMore)} halign={Gtk.Align.END} onClicked={() => {
((props.enableOnClickMore instanceof Accessor) ?
props.enableOnClickMore.get()
: props.enableOnClickMore) && props.onToggledOn?.();
props.onClickMore?.();
}} tooltipText={tr("control_center.tiles.more")} />
</Gtk.Box> as Gtk.Widget;
}