From 9e8634d892c559c5b44565a68bf35b13cbcb5572 Mon Sep 17 00:00:00 2001 From: Aylur Date: Thu, 16 Jan 2025 17:37:00 +0100 Subject: add: gtk3 ts popover example closes #224 closes #157 --- examples/gtk3/js/.gitignore | 7 + examples/gtk3/js/applauncher/README.md | 5 + examples/gtk3/js/applauncher/app.ts | 9 + examples/gtk3/js/applauncher/style.scss | 1 + .../gtk3/js/applauncher/widget/Applauncher.scss | 59 +++++ .../gtk3/js/applauncher/widget/Applauncher.tsx | 87 +++++++ examples/gtk3/js/media-player/README.md | 5 + examples/gtk3/js/media-player/app.ts | 11 + examples/gtk3/js/media-player/style.scss | 1 + .../gtk3/js/media-player/widget/MediaPlayer.scss | 56 +++++ .../gtk3/js/media-player/widget/MediaPlayer.tsx | 94 ++++++++ examples/gtk3/js/notifications/README.md | 5 + examples/gtk3/js/notifications/app.ts | 9 + .../notifications/notifications/Notification.scss | 125 ++++++++++ .../notifications/notifications/Notification.tsx | 107 +++++++++ .../notifications/NotificationPopups.tsx | 105 +++++++++ examples/gtk3/js/notifications/style.scss | 1 + examples/gtk3/js/popover/app.tsx | 104 +++++++++ examples/gtk3/js/simple-bar/README.md | 12 + examples/gtk3/js/simple-bar/app.ts | 13 ++ examples/gtk3/js/simple-bar/style.scss | 106 +++++++++ examples/gtk3/js/simple-bar/widget/Bar.tsx | 161 +++++++++++++ examples/gtk3/lua/simple-bar/README.md | 13 ++ examples/gtk3/lua/simple-bar/init.lua | 24 ++ examples/gtk3/lua/simple-bar/lib.lua | 25 ++ examples/gtk3/lua/simple-bar/style.scss | 106 +++++++++ examples/gtk3/lua/simple-bar/widget/Bar.lua | 198 ++++++++++++++++ examples/gtk3/py/.gitignore | 1 + examples/gtk3/py/simple-bar/README.md | 13 ++ examples/gtk3/py/simple-bar/__init__.py | 0 examples/gtk3/py/simple-bar/app.py | 36 +++ examples/gtk3/py/simple-bar/style.scss | 106 +++++++++ examples/gtk3/py/simple-bar/versions.py | 15 ++ examples/gtk3/py/simple-bar/widget/Bar.py | 251 ++++++++++++++++++++ examples/gtk3/py/simple-bar/widget/__init__.py | 0 examples/gtk3/vala/simple-bar/README.md | 13 ++ examples/gtk3/vala/simple-bar/app.in.vala | 30 +++ examples/gtk3/vala/simple-bar/flake.nix | 42 ++++ examples/gtk3/vala/simple-bar/meson.build | 45 ++++ examples/gtk3/vala/simple-bar/style.scss | 106 +++++++++ examples/gtk3/vala/simple-bar/widget/Bar.vala | 253 +++++++++++++++++++++ 41 files changed, 2360 insertions(+) create mode 100644 examples/gtk3/js/.gitignore create mode 100644 examples/gtk3/js/applauncher/README.md create mode 100644 examples/gtk3/js/applauncher/app.ts create mode 100644 examples/gtk3/js/applauncher/style.scss create mode 100644 examples/gtk3/js/applauncher/widget/Applauncher.scss create mode 100644 examples/gtk3/js/applauncher/widget/Applauncher.tsx create mode 100644 examples/gtk3/js/media-player/README.md create mode 100644 examples/gtk3/js/media-player/app.ts create mode 100644 examples/gtk3/js/media-player/style.scss create mode 100644 examples/gtk3/js/media-player/widget/MediaPlayer.scss create mode 100644 examples/gtk3/js/media-player/widget/MediaPlayer.tsx create mode 100644 examples/gtk3/js/notifications/README.md create mode 100644 examples/gtk3/js/notifications/app.ts create mode 100644 examples/gtk3/js/notifications/notifications/Notification.scss create mode 100644 examples/gtk3/js/notifications/notifications/Notification.tsx create mode 100644 examples/gtk3/js/notifications/notifications/NotificationPopups.tsx create mode 100644 examples/gtk3/js/notifications/style.scss create mode 100644 examples/gtk3/js/popover/app.tsx create mode 100644 examples/gtk3/js/simple-bar/README.md create mode 100644 examples/gtk3/js/simple-bar/app.ts create mode 100644 examples/gtk3/js/simple-bar/style.scss create mode 100644 examples/gtk3/js/simple-bar/widget/Bar.tsx create mode 100644 examples/gtk3/lua/simple-bar/README.md create mode 100644 examples/gtk3/lua/simple-bar/init.lua create mode 100644 examples/gtk3/lua/simple-bar/lib.lua create mode 100644 examples/gtk3/lua/simple-bar/style.scss create mode 100644 examples/gtk3/lua/simple-bar/widget/Bar.lua create mode 100644 examples/gtk3/py/.gitignore create mode 100644 examples/gtk3/py/simple-bar/README.md create mode 100644 examples/gtk3/py/simple-bar/__init__.py create mode 100755 examples/gtk3/py/simple-bar/app.py create mode 100644 examples/gtk3/py/simple-bar/style.scss create mode 100644 examples/gtk3/py/simple-bar/versions.py create mode 100644 examples/gtk3/py/simple-bar/widget/Bar.py create mode 100644 examples/gtk3/py/simple-bar/widget/__init__.py create mode 100644 examples/gtk3/vala/simple-bar/README.md create mode 100644 examples/gtk3/vala/simple-bar/app.in.vala create mode 100644 examples/gtk3/vala/simple-bar/flake.nix create mode 100644 examples/gtk3/vala/simple-bar/meson.build create mode 100644 examples/gtk3/vala/simple-bar/style.scss create mode 100644 examples/gtk3/vala/simple-bar/widget/Bar.vala (limited to 'examples/gtk3') diff --git a/examples/gtk3/js/.gitignore b/examples/gtk3/js/.gitignore new file mode 100644 index 0000000..d53b85b --- /dev/null +++ b/examples/gtk3/js/.gitignore @@ -0,0 +1,7 @@ +@girs/ +tsconfig.json +env.d.ts +dist/ +node_modules/ +package.json +package-lock.json diff --git a/examples/gtk3/js/applauncher/README.md b/examples/gtk3/js/applauncher/README.md new file mode 100644 index 0000000..682adf1 --- /dev/null +++ b/examples/gtk3/js/applauncher/README.md @@ -0,0 +1,5 @@ +# Applauncher + +![launcher](https://github.com/user-attachments/assets/2695e3bb-dff4-478a-b392-279fe638bfd3) + +Using [Apps](https://aylur.github.io/astal/guide/libraries/apps). diff --git a/examples/gtk3/js/applauncher/app.ts b/examples/gtk3/js/applauncher/app.ts new file mode 100644 index 0000000..d6c9e1c --- /dev/null +++ b/examples/gtk3/js/applauncher/app.ts @@ -0,0 +1,9 @@ +import { App } from "astal/gtk3" +import style from "./style.scss" +import Applauncher from "./widget/Applauncher" + +App.start({ + instanceName: "launcher", + css: style, + main: Applauncher, +}) diff --git a/examples/gtk3/js/applauncher/style.scss b/examples/gtk3/js/applauncher/style.scss new file mode 100644 index 0000000..ba13eed --- /dev/null +++ b/examples/gtk3/js/applauncher/style.scss @@ -0,0 +1 @@ +@use "./widget/Applauncher.scss" diff --git a/examples/gtk3/js/applauncher/widget/Applauncher.scss b/examples/gtk3/js/applauncher/widget/Applauncher.scss new file mode 100644 index 0000000..ae2453d --- /dev/null +++ b/examples/gtk3/js/applauncher/widget/Applauncher.scss @@ -0,0 +1,59 @@ +@use "sass:string"; + +@function gtkalpha($c, $a) { + @return string.unquote("alpha(#{$c},#{$a})"); +} + +// https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gtk/theme/Adwaita/_colors-public.scss +$fg-color: #{"@theme_fg_color"}; +$bg-color: #{"@theme_bg_color"}; + +window#launcher { + all: unset; + + box.Applauncher { + background-color: $bg-color; + border-radius: 11px; + margin: 1rem; + padding: .8rem; + box-shadow: 2px 3px 8px 0 gtkalpha(black, .4); + + entry { + margin-bottom: .8rem; + } + + button { + min-width: 0; + min-height: 0; + padding: .5rem; + + icon { + font-size: 3em; + margin-right: .3rem; + } + + label.name { + font-weight: bold; + font-size: 1.1em + } + + label.description { + color: gtkalpha($fg-color, .8); + } + } + + box.not-found { + padding: 1rem; + + icon { + font-size: 6em; + color: gtkalpha($fg-color, .7); + } + + label { + color: gtkalpha($fg-color, .9); + font-size: 1.2em; + } + } + } +} diff --git a/examples/gtk3/js/applauncher/widget/Applauncher.tsx b/examples/gtk3/js/applauncher/widget/Applauncher.tsx new file mode 100644 index 0000000..c7bac68 --- /dev/null +++ b/examples/gtk3/js/applauncher/widget/Applauncher.tsx @@ -0,0 +1,87 @@ +import Apps from "gi://AstalApps" +import { App, Astal, Gdk, Gtk } from "astal/gtk3" +import { Variable } from "astal" + +const MAX_ITEMS = 8 + +function hide() { + App.get_window("launcher")!.hide() +} + +function AppButton({ app }: { app: Apps.Application }) { + return +} + +export default function Applauncher() { + const { CENTER } = Gtk.Align + const apps = new Apps.Apps() + + const text = Variable("") + const list = text(text => apps.fuzzy_query(text).slice(0, MAX_ITEMS)) + const onEnter = () => { + apps.fuzzy_query(text.get())?.[0].launch() + hide() + } + + return text.set("")} + onKeyPressEvent={function (self, event: Gdk.Event) { + if (event.get_keyval()[1] === Gdk.KEY_Escape) + self.hide() + }}> + + + + + + text.set(self.text)} + onActivate={onEnter} + /> + + {list.as(list => list.map(app => ( + + )))} + + l.length === 0)}> + + + + + + + + +} diff --git a/examples/gtk3/js/media-player/README.md b/examples/gtk3/js/media-player/README.md new file mode 100644 index 0000000..4e3d237 --- /dev/null +++ b/examples/gtk3/js/media-player/README.md @@ -0,0 +1,5 @@ +# Media Player + +![mpris](https://github.com/user-attachments/assets/891e9706-74db-4505-bd83-c3628d7b4fd0) + +Using [Mpris](https://aylur.github.io/astal/guide/libraries/mpris). diff --git a/examples/gtk3/js/media-player/app.ts b/examples/gtk3/js/media-player/app.ts new file mode 100644 index 0000000..5b7558a --- /dev/null +++ b/examples/gtk3/js/media-player/app.ts @@ -0,0 +1,11 @@ +import { App, Widget } from "astal/gtk3" +import style from "./style.scss" +import MprisPlayers from "./widget/MediaPlayer" + +App.start({ + instanceName: "players", + css: style, + main: () => { + new Widget.Window({}, MprisPlayers()) + } +}) diff --git a/examples/gtk3/js/media-player/style.scss b/examples/gtk3/js/media-player/style.scss new file mode 100644 index 0000000..2e2f625 --- /dev/null +++ b/examples/gtk3/js/media-player/style.scss @@ -0,0 +1 @@ +@use "./widget/MediaPlayer.scss"; diff --git a/examples/gtk3/js/media-player/widget/MediaPlayer.scss b/examples/gtk3/js/media-player/widget/MediaPlayer.scss new file mode 100644 index 0000000..e1597c2 --- /dev/null +++ b/examples/gtk3/js/media-player/widget/MediaPlayer.scss @@ -0,0 +1,56 @@ +// https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gtk/theme/Adwaita/_colors-public.scss +$fg-color: #{"@theme_fg_color"}; +$bg-color: #{"@theme_bg_color"}; + +window { + all: unset; +} + +box.MediaPlayer { + padding: .6rem; + background-color: $bg-color; + + box.cover-art { + min-width: 120px; + min-height: 120px; + border-radius: 9px; + margin-right: .6rem; + background-size: contain; + background-position: center; + } + + box.title { + label { + font-weight: bold; + font-size: 1.1em; + } + } + + scale { + padding: 0; + margin: .4rem 0; + + trough { + min-height: 8px; + } + + highlight { + background-color: $fg-color; + } + + slider { + all: unset; + } + } + + centerbox.actions { + min-width: 220px; + + button { + min-width: 0; + min-height: 0; + padding: .4rem; + margin: 0 .2rem; + } + } +} diff --git a/examples/gtk3/js/media-player/widget/MediaPlayer.tsx b/examples/gtk3/js/media-player/widget/MediaPlayer.tsx new file mode 100644 index 0000000..06c7e77 --- /dev/null +++ b/examples/gtk3/js/media-player/widget/MediaPlayer.tsx @@ -0,0 +1,94 @@ +import { Astal, Gtk } from "astal/gtk3" +import Mpris from "gi://AstalMpris" +import { bind } from "astal" + +function lengthStr(length: number) { + const min = Math.floor(length / 60) + const sec = Math.floor(length % 60) + const sec0 = sec < 10 ? "0" : "" + return `${min}:${sec0}${sec}` +} + + +function MediaPlayer({ player }: { player: Mpris.Player }) { + const { START, END } = Gtk.Align + + const title = bind(player, "title").as(t => + t || "Unknown Track") + + const artist = bind(player, "artist").as(a => + a || "Unknown Artist") + + const coverArt = bind(player, "coverArt").as(c => + `background-image: url('${c}')`) + + const playerIcon = bind(player, "entry").as(e => + Astal.Icon.lookup_icon(e) ? e : "audio-x-generic-symbolic") + + const position = bind(player, "position").as(p => player.length > 0 + ? p / player.length : 0) + + const playIcon = bind(player, "playbackStatus").as(s => + s === Mpris.PlaybackStatus.PLAYING + ? "media-playback-pause-symbolic" + : "media-playback-start-symbolic" + ) + + return + + + + + + +} + +export default function MprisPlayers() { + const mpris = Mpris.get_default() + return + {bind(mpris, "players").as(arr => arr.map(player => ( + + )))} + +} diff --git a/examples/gtk3/js/notifications/README.md b/examples/gtk3/js/notifications/README.md new file mode 100644 index 0000000..60dad60 --- /dev/null +++ b/examples/gtk3/js/notifications/README.md @@ -0,0 +1,5 @@ +# Notifications Popups + +![notifs](https://github.com/user-attachments/assets/0df0eddc-5c74-4af0-a694-48dc8ec6bb44) + +A replacement for dunst and other daemons using [Notifd](https://aylur.github.io/astal/guide/libraries/notifd). diff --git a/examples/gtk3/js/notifications/app.ts b/examples/gtk3/js/notifications/app.ts new file mode 100644 index 0000000..ed53292 --- /dev/null +++ b/examples/gtk3/js/notifications/app.ts @@ -0,0 +1,9 @@ +import { App } from "astal/gtk3" +import style from "./style.scss" +import NotificationPopups from "./notifications/NotificationPopups" + +App.start({ + instanceName: "notifications", + css: style, + main: () => App.get_monitors().map(NotificationPopups), +}) diff --git a/examples/gtk3/js/notifications/notifications/Notification.scss b/examples/gtk3/js/notifications/notifications/Notification.scss new file mode 100644 index 0000000..a32f08b --- /dev/null +++ b/examples/gtk3/js/notifications/notifications/Notification.scss @@ -0,0 +1,125 @@ +@use "sass:string"; + +@function gtkalpha($c, $a) { + @return string.unquote("alpha(#{$c},#{$a})"); +} + +// https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gtk/theme/Adwaita/_colors-public.scss +$fg-color: #{"@theme_fg_color"}; +$bg-color: #{"@theme_bg_color"}; +$error: red; + +window.NotificationPopups { + all: unset; +} + +eventbox.Notification { + + &:first-child>box { + margin-top: 1rem; + } + + &:last-child>box { + margin-bottom: 1rem; + } + + // eventboxes can not take margins so we style its inner box instead + >box { + min-width: 400px; + border-radius: 13px; + background-color: $bg-color; + margin: .5rem 1rem .5rem 1rem; + box-shadow: 2px 3px 8px 0 gtkalpha(black, .4); + border: 1pt solid gtkalpha($fg-color, .03); + } + + &.critical>box { + border: 1pt solid gtkalpha($error, .4); + + .header { + + .app-name { + color: gtkalpha($error, .8); + + } + + .app-icon { + color: gtkalpha($error, .6); + } + } + } + + .header { + padding: .5rem; + color: gtkalpha($fg-color, 0.5); + + .app-icon { + margin: 0 .4rem; + } + + .app-name { + margin-right: .3rem; + font-weight: bold; + + &:first-child { + margin-left: .4rem; + } + } + + .time { + margin: 0 .4rem; + } + + button { + padding: .2rem; + min-width: 0; + min-height: 0; + } + } + + separator { + margin: 0 .4rem; + background-color: gtkalpha($fg-color, .1); + } + + .content { + margin: 1rem; + margin-top: .5rem; + + .summary { + font-size: 1.2em; + color: $fg-color; + } + + .body { + color: gtkalpha($fg-color, 0.8); + } + + .image { + border: 1px solid gtkalpha($fg-color, .02); + margin-right: .5rem; + border-radius: 9px; + min-width: 100px; + min-height: 100px; + background-size: cover; + background-position: center; + } + } + + .actions { + margin: 1rem; + margin-top: 0; + + button { + margin: 0 .3rem; + + &:first-child { + margin-left: 0; + } + + &:last-child { + margin-right: 0; + } + } + } +} diff --git a/examples/gtk3/js/notifications/notifications/Notification.tsx b/examples/gtk3/js/notifications/notifications/Notification.tsx new file mode 100644 index 0000000..5149d5b --- /dev/null +++ b/examples/gtk3/js/notifications/notifications/Notification.tsx @@ -0,0 +1,107 @@ +import { GLib } from "astal" +import { Gtk, Astal } from "astal/gtk3" +import { type EventBox } from "astal/gtk3/widget" +import Notifd from "gi://AstalNotifd" + +const isIcon = (icon: string) => + !!Astal.Icon.lookup_icon(icon) + +const fileExists = (path: string) => + GLib.file_test(path, GLib.FileTest.EXISTS) + +const time = (time: number, format = "%H:%M") => GLib.DateTime + .new_from_unix_local(time) + .format(format)! + +const urgency = (n: Notifd.Notification) => { + const { LOW, NORMAL, CRITICAL } = Notifd.Urgency + // match operator when? + switch (n.urgency) { + case LOW: return "low" + case CRITICAL: return "critical" + case NORMAL: + default: return "normal" + } +} + +type Props = { + setup(self: EventBox): void + onHoverLost(self: EventBox): void + notification: Notifd.Notification +} + +export default function Notification(props: Props) { + const { notification: n, onHoverLost, setup } = props + const { START, CENTER, END } = Gtk.Align + + return + + + {(n.appIcon || n.desktopEntry) && } + + + + {n.image && fileExists(n.image) && } + {n.image && isIcon(n.image) && + + } + + + + {n.get_actions().length > 0 && + {n.get_actions().map(({ label, id }) => ( + + ))} + } + + +} diff --git a/examples/gtk3/js/notifications/notifications/NotificationPopups.tsx b/examples/gtk3/js/notifications/notifications/NotificationPopups.tsx new file mode 100644 index 0000000..13fdd88 --- /dev/null +++ b/examples/gtk3/js/notifications/notifications/NotificationPopups.tsx @@ -0,0 +1,105 @@ +import { Astal, Gtk, Gdk } from "astal/gtk3" +import Notifd from "gi://AstalNotifd" +import Notification from "./Notification" +import { type Subscribable } from "astal/binding" +import { Variable, bind, timeout } from "astal" + +// see comment below in constructor +const TIMEOUT_DELAY = 5000 + +// The purpose if this class is to replace Variable> +// with a Map type in order to track notification widgets +// by their id, while making it conviniently bindable as an array +class NotifiationMap implements Subscribable { + // the underlying map to keep track of id widget pairs + private map: Map = new Map() + + // it makes sense to use a Variable under the hood and use its + // reactivity implementation instead of keeping track of subscribers ourselves + private var: Variable> = Variable([]) + + // notify subscribers to rerender when state changes + private notifiy() { + this.var.set([...this.map.values()].reverse()) + } + + constructor() { + const notifd = Notifd.get_default() + + /** + * uncomment this if you want to + * ignore timeout by senders and enforce our own timeout + * note that if the notification has any actions + * they might not work, since the sender already treats them as resolved + */ + // notifd.ignoreTimeout = true + + notifd.connect("notified", (_, id) => { + this.set(id, Notification({ + notification: notifd.get_notification(id)!, + + // once hovering over the notification is done + // destroy the widget without calling notification.dismiss() + // so that it acts as a "popup" and we can still display it + // in a notification center like widget + // but clicking on the close button will close it + onHoverLost: () => this.delete(id), + + // notifd by default does not close notifications + // until user input or the timeout specified by sender + // which we set to ignore above + setup: () => timeout(TIMEOUT_DELAY, () => { + /** + * uncomment this if you want to "hide" the notifications + * after TIMEOUT_DELAY + */ + // this.delete(id) + }) + })) + }) + + // notifications can be closed by the outside before + // any user input, which have to be handled too + notifd.connect("resolved", (_, id) => { + this.delete(id) + }) + } + + private set(key: number, value: Gtk.Widget) { + // in case of replacecment destroy previous widget + this.map.get(key)?.destroy() + this.map.set(key, value) + this.notifiy() + } + + private delete(key: number) { + this.map.get(key)?.destroy() + this.map.delete(key) + this.notifiy() + } + + // needed by the Subscribable interface + get() { + return this.var.get() + } + + // needed by the Subscribable interface + subscribe(callback: (list: Array) => void) { + return this.var.subscribe(callback) + } +} + +export default function NotificationPopups(gdkmonitor: Gdk.Monitor) { + const { TOP, RIGHT } = Astal.WindowAnchor + const notifs = new NotifiationMap() + + return + + {bind(notifs)} + + +} diff --git a/examples/gtk3/js/notifications/style.scss b/examples/gtk3/js/notifications/style.scss new file mode 100644 index 0000000..7ef0168 --- /dev/null +++ b/examples/gtk3/js/notifications/style.scss @@ -0,0 +1 @@ +@use "./notifications/Notification.scss"; diff --git a/examples/gtk3/js/popover/app.tsx b/examples/gtk3/js/popover/app.tsx new file mode 100644 index 0000000..6358297 --- /dev/null +++ b/examples/gtk3/js/popover/app.tsx @@ -0,0 +1,104 @@ +import { App, Astal, Gdk, Gtk } from "astal/gtk3" + +const { TOP, RIGHT, BOTTOM, LEFT } = Astal.WindowAnchor + +type PopupProps = { + child?: unknown + marginBottom?: number + marginTop?: number + marginLeft?: number + marginRight?: number + halign?: Gtk.Align + valign?: Gtk.Align +} + +function Popup({ + child, + marginBottom, + marginTop, + marginLeft, + marginRight, + halign = Gtk.Align.CENTER, + valign = Gtk.Align.CENTER, +}: PopupProps) { + return ( + { + const [, _x, _y] = event.get_coords() + const { x, y, width, height } = self + .get_child()! + .get_allocation() + + const xOut = _x < x || _x > x + width + const yOut = _y < y || _y > y + height + + // clicked outside + if (xOut || yOut) self.hide() + }} + // close when hitting Escape + onKeyPressEvent={(self, event: Gdk.Event) => { + if (event.get_keyval()[1] === Gdk.KEY_Escape) { + self.hide() + } + }} + > + true} // make sure click event does not bubble up + // child can be positioned with `halign` `valign` and margins + expand + halign={halign} + valign={valign} + marginBottom={marginBottom} + marginTop={marginTop} + marginStart={marginLeft} + marginEnd={marginRight} + > + {child} + + + ) +} + +App.start({ + instanceName: "popup-example", + css: ` + .Popup { + background-color: @theme_bg_color; + box-shadow: 2px 3px 7px 0 rgba(0,0,0,0.4); + border-radius: 12px; + padding: 12px; + } + `, + main() { + const popup = ( + + + + ) + + return ( + + + + ) + }, +}) diff --git a/examples/gtk3/js/simple-bar/README.md b/examples/gtk3/js/simple-bar/README.md new file mode 100644 index 0000000..f92b20e --- /dev/null +++ b/examples/gtk3/js/simple-bar/README.md @@ -0,0 +1,12 @@ +# Simple Bar Example + +![simple-bar](https://github.com/user-attachments/assets/a306c864-56b7-44c4-8820-81f424f32b9b) + +A simple bar for Hyprland using + +- [Battery library](https://aylur.github.io/astal/guide/libraries/battery). +- [Hyprland library](https://aylur.github.io/astal/guide/libraries/hyprland). +- [Mpris library](https://aylur.github.io/astal/guide/libraries/mpris). +- [Network library](https://aylur.github.io/astal/guide/libraries/network). +- [Tray library](https://aylur.github.io/astal/guide/libraries/tray). +- [WirePlumber library](https://aylur.github.io/astal/guide/libraries/wireplumber). diff --git a/examples/gtk3/js/simple-bar/app.ts b/examples/gtk3/js/simple-bar/app.ts new file mode 100644 index 0000000..4b7ea48 --- /dev/null +++ b/examples/gtk3/js/simple-bar/app.ts @@ -0,0 +1,13 @@ +import { App } from "astal/gtk3" +import style from "./style.scss" +import Bar from "./widget/Bar" + +App.start({ + css: style, + instanceName: "js", + requestHandler(request, res) { + print(request) + res("ok") + }, + main: () => App.get_monitors().map(Bar), +}) diff --git a/examples/gtk3/js/simple-bar/style.scss b/examples/gtk3/js/simple-bar/style.scss new file mode 100644 index 0000000..f5f771a --- /dev/null +++ b/examples/gtk3/js/simple-bar/style.scss @@ -0,0 +1,106 @@ +@use "sass:color"; + +$bg: #212223; +$fg: #f1f1f1; +$accent: #378DF7; +$radius: 7px; + +window.Bar { + border: none; + box-shadow: none; + background-color: $bg; + color: $fg; + font-size: 1.1em; + font-weight: bold; + + label { + margin: 0 8px; + } + + .Workspaces { + button { + all: unset; + background-color: transparent; + + &:hover label { + background-color: color.adjust($fg, $alpha: -0.84); + border-color: color.adjust($accent, $alpha: -0.8); + } + + &:active label { + background-color: color.adjust($fg, $alpha: -0.8) + } + } + + label { + transition: 200ms; + padding: 0 8px; + margin: 2px; + border-radius: $radius; + border: 1pt solid transparent; + } + + .focused label { + color: $accent; + border-color: $accent; + } + } + + .SysTray { + margin-right: 8px; + + button { + padding: 0 4px; + } + } + + .FocusedClient { + color: $accent; + } + + .Media .Cover { + min-height: 1.2em; + min-width: 1.2em; + border-radius: $radius; + background-position: center; + background-size: contain; + } + + .Battery label { + padding-left: 0; + margin-left: 0; + } + + .AudioSlider { + * { + all: unset; + } + + icon { + margin-right: .6em; + } + + & { + margin: 0 1em; + } + + trough { + background-color: color.adjust($fg, $alpha: -0.8); + border-radius: $radius; + } + + highlight { + background-color: $accent; + min-height: .8em; + border-radius: $radius; + } + + slider { + background-color: $fg; + border-radius: $radius; + min-height: 1em; + min-width: 1em; + margin: -.2em; + } + } +} diff --git a/examples/gtk3/js/simple-bar/widget/Bar.tsx b/examples/gtk3/js/simple-bar/widget/Bar.tsx new file mode 100644 index 0000000..6592f6a --- /dev/null +++ b/examples/gtk3/js/simple-bar/widget/Bar.tsx @@ -0,0 +1,161 @@ +import { App } from "astal/gtk3" +import { Variable, GLib, bind } from "astal" +import { Astal, Gtk, Gdk } from "astal/gtk3" +import Hyprland from "gi://AstalHyprland" +import Mpris from "gi://AstalMpris" +import Battery from "gi://AstalBattery" +import Wp from "gi://AstalWp" +import Network from "gi://AstalNetwork" +import Tray from "gi://AstalTray" + +function SysTray() { + const tray = Tray.get_default() + + return + {bind(tray, "items").as(items => items.map(item => ( + ["dbusmenu", ag])} + menuModel={bind(item, "menuModel")}> + + + )))} + +} + +function Wifi() { + const network = Network.get_default() + const wifi = bind(network, "wifi") + + return + {wifi.as(wifi => wifi && ( + + ))} + + +} + +function AudioSlider() { + const speaker = Wp.get_default()?.audio.defaultSpeaker! + + return + + speaker.volume = value} + value={bind(speaker, "volume")} + /> + +} + +function BatteryLevel() { + const bat = Battery.get_default() + + return + + +} + +function Media() { + const mpris = Mpris.get_default() + + return + {bind(mpris, "players").as(ps => ps[0] ? ( + + + `background-image: url('${cover}');` + )} + /> + + ) : ( + "Nothing Playing" + ))} + +} + +function Workspaces() { + const hypr = Hyprland.get_default() + + return + {bind(hypr, "workspaces").as(wss => wss + .filter(ws => !(ws.id >= -99 && ws.id <= -2)) // filter out special workspaces + .sort((a, b) => a.id - b.id) + .map(ws => ( + + )) + )} + +} + +function FocusedClient() { + const hypr = Hyprland.get_default() + const focused = bind(hypr, "focusedClient") + + return + {focused.as(client => ( + client && +} + +function Time({ format = "%H:%M - %A %e." }) { + const time = Variable("").poll(1000, () => + GLib.DateTime.new_now_local().format(format)!) + + return