mirror of
https://github.com/belsabbagh/dotfiles.git
synced 2026-04-11 09:36:46 +00:00
quickshell and hyprland additions
This commit is contained in:
24
.config/quickshell/nucleus-shell/CMakeLists.txt
Normal file
24
.config/quickshell/nucleus-shell/CMakeLists.txt
Normal file
@@ -0,0 +1,24 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
project(nucleus-shell VERSION 1.0.0 LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
find_package(Qt6 6.5 COMPONENTS Quick REQUIRED)
|
||||
|
||||
qt_standard_project_setup(REQUIRES 6.5)
|
||||
|
||||
qt_add_executable(nucleus-shell
|
||||
main.cpp
|
||||
)
|
||||
|
||||
qt_add_qml_module(nucleus-shell
|
||||
URI nucleus-shell
|
||||
QML_FILES
|
||||
shell.qml
|
||||
RESOURCES
|
||||
img/world.png
|
||||
)
|
||||
|
||||
target_link_libraries(nucleus-shell PRIVATE Qt6::Quick)
|
||||
BIN
.config/quickshell/nucleus-shell/assets/gifs/bongo-cat.gif
Normal file
BIN
.config/quickshell/nucleus-shell/assets/gifs/bongo-cat.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
262
.config/quickshell/nucleus-shell/config/Appearance.qml
Normal file
262
.config/quickshell/nucleus-shell/config/Appearance.qml
Normal file
@@ -0,0 +1,262 @@
|
||||
import qs.modules.functions
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
property QtObject m3colors
|
||||
property QtObject colors
|
||||
property QtObject rounding
|
||||
property QtObject font
|
||||
property QtObject margin
|
||||
property QtObject moduleLayouts
|
||||
property QtObject animation
|
||||
property string syntaxHighlightingTheme
|
||||
|
||||
readonly property bool darkmode: Config.runtime.appearance.theme === "dark"
|
||||
readonly property bool transparentize: Config.runtime.appearance.transparency.enabled
|
||||
readonly property double alpha: Config.runtime.appearance.transparency.alpha
|
||||
|
||||
colors: QtObject {
|
||||
property color colSubtext: m3colors.m3outline
|
||||
property color colLayer0: m3colors.m3background
|
||||
property color colLayer0Border: ColorUtils.mix(root.m3colors.m3outlineVariant, colLayer0, 0.4)
|
||||
property color colLayer1: m3colors.m3surfaceContainerLow
|
||||
property color colOnLayer1: m3colors.m3onSurfaceVariant
|
||||
property color colOnLayer1Inactive: ColorUtils.mix(colOnLayer1, colLayer1, 0.45)
|
||||
property color colLayer1Hover: ColorUtils.mix(colLayer1, colOnLayer1, 0.92)
|
||||
property color colLayer1Active: ColorUtils.mix(colLayer1, colOnLayer1, 0.85)
|
||||
property color colLayer2: m3colors.m3surfaceContainer
|
||||
property color colOnLayer2: m3colors.m3onSurface
|
||||
property color colLayer2Hover: ColorUtils.mix(colLayer2, colOnLayer2, 0.90)
|
||||
property color colLayer2Active: ColorUtils.mix(colLayer2, colOnLayer2, 0.80)
|
||||
property color colPrimary: m3colors.m3primary
|
||||
property color colOnPrimary: m3colors.m3onPrimary
|
||||
property color colSecondary: m3colors.m3secondary
|
||||
property color colSecondaryContainer: m3colors.m3secondaryContainer
|
||||
property color colOnSecondaryContainer: m3colors.m3onSecondaryContainer
|
||||
property color colTooltip: m3colors.m3inverseSurface
|
||||
property color colOnTooltip: m3colors.m3inverseOnSurface
|
||||
property color colShadow: ColorUtils.transparentize(m3colors.m3shadow, 0.7)
|
||||
property color colOutline: m3colors.m3outline
|
||||
}
|
||||
|
||||
m3colors: QtObject {
|
||||
function t(c) {
|
||||
return root.transparentize
|
||||
? ColorUtils.transparentize(c, root.alpha)
|
||||
: c
|
||||
}
|
||||
|
||||
function tH(c) {
|
||||
return root.transparentize
|
||||
? ColorUtils.transparentize(c, root.alpha + 1) // Totally transparent
|
||||
: c
|
||||
}
|
||||
|
||||
readonly property color m3background: t(MaterialColors.colors.background)
|
||||
readonly property color m3paddingContainer: t(Config.runtime.bar.modules.paddingColor)
|
||||
readonly property color m3surface: t(MaterialColors.colors.surface)
|
||||
readonly property color m3surfaceDim: t(MaterialColors.colors.surface_dim)
|
||||
readonly property color m3surfaceBright: t(MaterialColors.colors.surface_bright)
|
||||
|
||||
readonly property color m3surfaceContainerLowest: tH(MaterialColors.colors.surface_container_lowest)
|
||||
readonly property color m3surfaceContainerLow: tH(MaterialColors.colors.surface_container_low)
|
||||
readonly property color m3surfaceContainer: tH(MaterialColors.colors.surface_container)
|
||||
readonly property color m3surfaceContainerHigh: tH(MaterialColors.colors.surface_container_high)
|
||||
readonly property color m3surfaceContainerHighest: tH(MaterialColors.colors.surface_container_highest)
|
||||
|
||||
readonly property color m3onSurface: t(MaterialColors.colors.on_surface)
|
||||
readonly property color m3surfaceVariant: t(MaterialColors.colors.surface_variant)
|
||||
readonly property color m3onSurfaceVariant: t(MaterialColors.colors.on_surface_variant)
|
||||
|
||||
readonly property color m3inverseSurface: t(MaterialColors.colors.inverse_surface)
|
||||
readonly property color m3inverseOnSurface: t(MaterialColors.colors.inverse_on_surface)
|
||||
|
||||
readonly property color m3outline: t(MaterialColors.colors.outline)
|
||||
readonly property color m3outlineVariant: t(MaterialColors.colors.outline_variant)
|
||||
readonly property color m3shadow: t(MaterialColors.colors.shadow)
|
||||
readonly property color m3scrim: t(MaterialColors.colors.scrim)
|
||||
readonly property color m3surfaceTint: t(MaterialColors.colors.surface_tint)
|
||||
|
||||
readonly property color m3primary: t(MaterialColors.colors.primary)
|
||||
readonly property color m3onPrimary: t(MaterialColors.colors.on_primary)
|
||||
readonly property color m3primaryContainer: t(MaterialColors.colors.primary_container)
|
||||
readonly property color m3onPrimaryContainer: t(MaterialColors.colors.on_primary_container)
|
||||
readonly property color m3inversePrimary: t(MaterialColors.colors.inverse_primary)
|
||||
|
||||
readonly property color m3secondary: t(MaterialColors.colors.secondary)
|
||||
readonly property color m3onSecondary: t(MaterialColors.colors.on_secondary)
|
||||
readonly property color m3secondaryContainer: t(MaterialColors.colors.secondary_container)
|
||||
readonly property color m3onSecondaryContainer: t(MaterialColors.colors.on_secondary_container)
|
||||
|
||||
readonly property color m3tertiary: t(MaterialColors.colors.tertiary)
|
||||
readonly property color m3onTertiary: t(MaterialColors.colors.on_tertiary)
|
||||
readonly property color m3tertiaryContainer: t(MaterialColors.colors.tertiary_container)
|
||||
readonly property color m3onTertiaryContainer: t(MaterialColors.colors.on_tertiary_container)
|
||||
|
||||
readonly property color m3error: t(MaterialColors.colors.error)
|
||||
readonly property color m3onError: t(MaterialColors.colors.on_error)
|
||||
readonly property color m3errorContainer: t(MaterialColors.colors.error_container)
|
||||
readonly property color m3onErrorContainer: t(MaterialColors.colors.on_error_container)
|
||||
|
||||
readonly property color m3primaryFixed: t(MaterialColors.colors.primary_fixed)
|
||||
readonly property color m3primaryFixedDim: t(MaterialColors.colors.primary_fixed_dim)
|
||||
readonly property color m3onPrimaryFixed: t(MaterialColors.colors.on_primary_fixed)
|
||||
readonly property color m3onPrimaryFixedVariant: t(MaterialColors.colors.on_primary_fixed_variant)
|
||||
|
||||
readonly property color m3secondaryFixed: t(MaterialColors.colors.secondary_fixed)
|
||||
readonly property color m3secondaryFixedDim: t(MaterialColors.colors.secondary_fixed_dim)
|
||||
readonly property color m3onSecondaryFixed: t(MaterialColors.colors.on_secondary_fixed)
|
||||
readonly property color m3onSecondaryFixedVariant: t(MaterialColors.colors.on_secondary_fixed_variant)
|
||||
|
||||
readonly property color m3tertiaryFixed: t(MaterialColors.colors.tertiary_fixed)
|
||||
readonly property color m3tertiaryFixedDim: t(MaterialColors.colors.tertiary_fixed_dim)
|
||||
readonly property color m3onTertiaryFixed: t(MaterialColors.colors.on_tertiary_fixed)
|
||||
readonly property color m3onTertiaryFixedVariant: t(MaterialColors.colors.on_tertiary_fixed_variant)
|
||||
|
||||
}
|
||||
|
||||
|
||||
margin: QtObject {
|
||||
property int supertiny: 2
|
||||
property int tinier: 3
|
||||
property int tiny: 6
|
||||
property int verysmall: 8
|
||||
property int small: 12
|
||||
property int normal: 16
|
||||
property int large: 22
|
||||
property int verylarge: 30
|
||||
property int extraLarge: 35
|
||||
}
|
||||
|
||||
animation: QtObject {
|
||||
|
||||
property var easing: Easing.OutExpo
|
||||
|
||||
property QtObject durations: QtObject {
|
||||
property int supershort: 100
|
||||
property int small: 200
|
||||
property int normal: 400
|
||||
property int large: 600
|
||||
property int extraLarge: 1000
|
||||
property int expressiveFastSpatial: 350
|
||||
property int expressiveDefaultSpatial: 500
|
||||
property int expressiveEffects: 200
|
||||
}
|
||||
|
||||
property QtObject curves: QtObject {
|
||||
readonly property list<real> expressiveFastSpatial: [0.42, 1.67, 0.21, 0.90, 1, 1] // Default, 350ms
|
||||
readonly property list<real> expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1.00, 1, 1] // Default, 500ms
|
||||
readonly property list<real> expressiveSlowSpatial: [0.39, 1.29, 0.35, 0.98, 1, 1] // Default, 650ms
|
||||
readonly property list<real> expressiveEffects: [0.34, 0.80, 0.34, 1.00, 1, 1] // Default, 200ms
|
||||
readonly property list<real> emphasized: [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1]
|
||||
readonly property list<real> emphasizedFirstHalf: [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82]
|
||||
readonly property list<real> emphasizedLastHalf: [5 / 24, 0.82, 0.25, 1, 1, 1]
|
||||
readonly property list<real> emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1]
|
||||
readonly property list<real> emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1]
|
||||
readonly property list<real> standard: [0.2, 0, 0, 1, 1, 1]
|
||||
readonly property list<real> standardAccel: [0.3, 0, 1, 1, 1, 1]
|
||||
readonly property list<real> standardDecel: [0, 0, 0, 1, 1, 1]
|
||||
readonly property real expressiveFastSpatialDuration: 350
|
||||
readonly property real expressiveDefaultSpatialDuration: 500
|
||||
readonly property real expressiveSlowSpatialDuration: 650
|
||||
readonly property real expressiveEffectsDuration: 200
|
||||
}
|
||||
|
||||
property QtObject elementMove: QtObject {
|
||||
property int duration: animation.curves.expressiveDefaultSpatialDuration
|
||||
property int type: Easing.BezierSpline
|
||||
property list<real> bezierCurve: animation.curves.expressiveDefaultSpatial
|
||||
property Component numberAnimation: Component {
|
||||
NumberAnimation {
|
||||
duration: root.animation.elementMove.duration
|
||||
easing.type: root.animation.elementMove.type
|
||||
easing.bezierCurve: root.animation.elementMove.bezierCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property QtObject elementMoveEnter: QtObject {
|
||||
property int duration: 400
|
||||
property int type: Easing.BezierSpline
|
||||
property list<real> bezierCurve: animation.curves.emphasizedDecel
|
||||
property Component numberAnimation: Component {
|
||||
NumberAnimation {
|
||||
duration: root.animation.elementMoveEnter.duration
|
||||
easing.type: root.animation.elementMoveEnter.type
|
||||
easing.bezierCurve: root.animation.elementMoveEnter.bezierCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property QtObject elementMoveFast: QtObject {
|
||||
property int duration: animation.curves.expressiveEffectsDuration
|
||||
property int type: Easing.BezierSpline
|
||||
property list<real> bezierCurve: animation.curves.expressiveEffects
|
||||
property Component numberAnimation: Component {
|
||||
NumberAnimation {
|
||||
duration: root.animation.elementMoveFast.duration
|
||||
easing.type: root.animation.elementMoveFast.type
|
||||
easing.bezierCurve: root.animation.elementMoveFast.bezierCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
rounding: QtObject {
|
||||
property int unsharpen: 2 * Config.runtime.appearance.rounding.factor
|
||||
property int unsharpenmore: 6 * Config.runtime.appearance.rounding.factor
|
||||
property int verysmall: 8 * Config.runtime.appearance.rounding.factor
|
||||
property int small: 12 * Config.runtime.appearance.rounding.factor
|
||||
property int normal: 17 * Config.runtime.appearance.rounding.factor
|
||||
property int large: 23 * Config.runtime.appearance.rounding.factor
|
||||
property int verylarge: 30 * Config.runtime.appearance.rounding.factor
|
||||
property int childish: 50 * Config.runtime.appearance.rounding.factor // Idk why did I named this childish
|
||||
property int full: 9999
|
||||
property int screenRounding: large
|
||||
property int windowRounding: 18 * Config.runtime.appearance.rounding.factor
|
||||
}
|
||||
|
||||
font: QtObject {
|
||||
property QtObject family: QtObject {
|
||||
property string main: Config.runtime.appearance.font.families.main
|
||||
property string title: Config.runtime.appearance.font.families.title
|
||||
property string materialIcons: Config.runtime.appearance.font.families.materialIcons
|
||||
property string nerdIcons: Config.runtime.appearance.font.families.nerdFonts
|
||||
property string monospace: Config.runtime.appearance.font.families.monospace
|
||||
property string reading: Config.runtime.appearance.font.families.reading
|
||||
property string expressive: Config.runtime.appearance.font.families.expressive
|
||||
}
|
||||
property QtObject size: QtObject {
|
||||
property int smallest: 10
|
||||
property int smaller: 12
|
||||
property int smallie: 13
|
||||
property int small: 15
|
||||
property int normal: 16
|
||||
property int large: 17
|
||||
property int larger: 19
|
||||
property int big: 21
|
||||
property int huge: 22
|
||||
property int hugeass: 23
|
||||
property int wildass: 40
|
||||
property int title: huge
|
||||
|
||||
property QtObject icon: QtObject {
|
||||
property int smallest: 10
|
||||
property int smaller: 12
|
||||
property int small: 14
|
||||
property int normal: 16
|
||||
property int large: 17
|
||||
property int larger: 19
|
||||
property int huge: 22
|
||||
property int hugeass: 23
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
syntaxHighlightingTheme: darkmode ? '#f1ebeb' : "#141333"
|
||||
}
|
||||
282
.config/quickshell/nucleus-shell/config/Config.qml
Normal file
282
.config/quickshell/nucleus-shell/config/Config.qml
Normal file
@@ -0,0 +1,282 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import QtCore
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.plugins
|
||||
import qs.services
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
property string filePath: Directories.shellConfigPath
|
||||
property alias runtime: configOptionsJsonAdapter
|
||||
property bool initialized: false
|
||||
property int readWriteDelay: 50
|
||||
property bool blockWrites: false
|
||||
|
||||
function updateKey(nestedKey, value) {
|
||||
let keys = nestedKey.split(".")
|
||||
let obj = root.runtime
|
||||
if (!obj) {
|
||||
console.warn("Config.updateKey: adapter not available for key", nestedKey)
|
||||
return
|
||||
}
|
||||
|
||||
for (let i = 0; i < keys.length - 1; ++i) {
|
||||
let k = keys[i]
|
||||
if (obj[k] === undefined || obj[k] === null || typeof obj[k] !== "object") {
|
||||
obj[k] = {} // Use Plain JS for serialization
|
||||
}
|
||||
obj = obj[k]
|
||||
if (!obj) {
|
||||
console.warn("Config.updateKey: failed to resolve", k)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let convertedValue = value
|
||||
if (typeof value === "string") {
|
||||
let trimmed = value.trim()
|
||||
if (trimmed === "true" || trimmed === "false" || (!isNaN(Number(trimmed)) && trimmed !== "")) {
|
||||
try {
|
||||
convertedValue = JSON.parse(trimmed)
|
||||
} catch (e) {
|
||||
convertedValue = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
obj[keys[keys.length - 1]] = convertedValue
|
||||
configFileView.adapterUpdated()
|
||||
}
|
||||
|
||||
function loadPluginConfigs(plugins) {
|
||||
console.log("Loading plugins:", plugins)
|
||||
|
||||
if (!root.runtime)
|
||||
return
|
||||
|
||||
if (!root.runtime.plugins)
|
||||
root.runtime.plugins = {}
|
||||
|
||||
function mergeDefaults(target, defaults) {
|
||||
let changed = false
|
||||
|
||||
for (let key in defaults) {
|
||||
const defVal = defaults[key]
|
||||
const tgtVal = target[key]
|
||||
|
||||
if (tgtVal === undefined) {
|
||||
target[key] = defVal
|
||||
changed = true
|
||||
} else if (
|
||||
typeof tgtVal === "object" &&
|
||||
typeof defVal === "object" &&
|
||||
tgtVal !== null &&
|
||||
defVal !== null
|
||||
) {
|
||||
if (mergeDefaults(tgtVal, defVal))
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
|
||||
return changed
|
||||
}
|
||||
|
||||
let anyChange = false
|
||||
|
||||
for (let i = 0; i < plugins.length; i++) {
|
||||
const name = plugins[i]
|
||||
const path = Directories.shellConfig + "/plugins/" + name + "/PluginConfigData.qml"
|
||||
|
||||
const component = Qt.createComponent(path)
|
||||
if (component.status === Component.Error) {
|
||||
console.warn("Plugin failed:", path, component.errorString())
|
||||
continue
|
||||
}
|
||||
|
||||
if (component.status !== Component.Ready)
|
||||
continue
|
||||
|
||||
const pluginObj = component.createObject(root)
|
||||
if (!pluginObj) {
|
||||
console.warn("Failed to create plugin object:", name)
|
||||
component.destroy()
|
||||
continue
|
||||
}
|
||||
|
||||
if (!pluginObj.defaults)
|
||||
pluginObj.defaults = { enabled: false }
|
||||
|
||||
if (!root.runtime.plugins[name]) {
|
||||
root.runtime.plugins[name] = {}
|
||||
anyChange = true
|
||||
}
|
||||
|
||||
if (mergeDefaults(root.runtime.plugins[name], pluginObj.defaults))
|
||||
anyChange = true
|
||||
|
||||
console.log("Plugin config injected:", name)
|
||||
|
||||
pluginObj.destroy()
|
||||
component.destroy()
|
||||
}
|
||||
|
||||
if (anyChange) {
|
||||
console.log("Plugin defaults merged, writing config")
|
||||
configFileView.adapterUpdated()
|
||||
} else {
|
||||
console.log("Plugin configs already up to date")
|
||||
}
|
||||
}
|
||||
|
||||
Timer { id: fileReloadTimer; interval: root.readWriteDelay; repeat: false; onTriggered: configFileView.reload() }
|
||||
Timer { id: fileWriteTimer; interval: root.readWriteDelay; repeat: false; onTriggered: configFileView.writeAdapter() }
|
||||
|
||||
Timer { // Used to output all log/debug to the terminal
|
||||
interval: 1200
|
||||
running: true
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
console.log("Injecting plugin configs")
|
||||
root.loadPluginConfigs(PluginLoader.plugins)
|
||||
console.log("Detected Compositor:", Compositor.detectedCompositor)
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: configFileView
|
||||
path: root.filePath
|
||||
watchChanges: true
|
||||
blockWrites: root.blockWrites
|
||||
onFileChanged: fileReloadTimer.restart()
|
||||
onAdapterUpdated: fileWriteTimer.restart()
|
||||
onLoaded: { root.initialized = true }
|
||||
onLoadFailed: error => {
|
||||
if (error == FileViewError.FileNotFound) writeAdapter()
|
||||
}
|
||||
|
||||
JsonAdapter {
|
||||
id: configOptionsJsonAdapter
|
||||
|
||||
property var plugins: ({}) // dynamic plugins config variable
|
||||
property var monitors: ({}) // per-monitor configuration for bars and wallpapers
|
||||
|
||||
property JsonObject appearance: JsonObject {
|
||||
property string theme: "dark"
|
||||
property bool tintIcons: false
|
||||
property JsonObject animations: JsonObject { property bool enabled: true; property double durationScale: 1 }
|
||||
property JsonObject transparency: JsonObject { property bool enabled: false; property double alpha: 0.2 }
|
||||
property JsonObject rounding: JsonObject { property double factor: 1 }
|
||||
property JsonObject font: JsonObject {
|
||||
property double scale: 1
|
||||
property JsonObject families: JsonObject {
|
||||
property string main: "JetBrains Mono"
|
||||
property string title: "Gabarito"
|
||||
property string materialIcons: "Material Symbols Rounded"
|
||||
property string nerdFonts: "JetBrains Mono NF"
|
||||
property string monospace: "JetBrains Mono NF"
|
||||
property string reading: "Readex Pro"
|
||||
property string expressive: "Space Grotesk"
|
||||
}
|
||||
}
|
||||
property JsonObject colors: JsonObject {
|
||||
property string scheme: "catppuccin-lavender"
|
||||
property string matugenScheme: "scheme-neutral"
|
||||
property bool autogenerated: true
|
||||
property bool runMatugenUserWide: false
|
||||
}
|
||||
property JsonObject background: JsonObject {
|
||||
property bool enabled: true
|
||||
property url defaultPath: Directories.defaultsPath + "/default.jpg"
|
||||
property JsonObject parallax: JsonObject {
|
||||
property bool enabled: true
|
||||
property bool enableSidebarLeft: true
|
||||
property bool enableSidebarRight: true
|
||||
property real zoom: 1.10
|
||||
}
|
||||
property JsonObject clock: JsonObject {
|
||||
property bool enabled: true
|
||||
property bool isAnalog: true
|
||||
property bool rotatePolygonBg: false
|
||||
property int rotationDuration: 18 // lower the faster
|
||||
property int edgeSpacing: 50
|
||||
property int shape: 1
|
||||
property int xPos: 0
|
||||
property int yPos: 0
|
||||
property bool animateHands: false
|
||||
}
|
||||
property JsonObject slideshow: JsonObject {
|
||||
property bool enabled: false
|
||||
property bool includeSubfolders: true
|
||||
property int interval: 5
|
||||
property string folder: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property JsonObject misc: JsonObject {
|
||||
property url pfp: Quickshell.env("HOME") + "/.face.icon"
|
||||
property JsonObject intelligence: JsonObject {
|
||||
property bool enabled: true
|
||||
property string apiKey: ""
|
||||
}
|
||||
}
|
||||
|
||||
property JsonObject notifications: JsonObject {
|
||||
property bool enabled: true
|
||||
property bool doNotDisturb: false
|
||||
property string position: "center"
|
||||
}
|
||||
property JsonObject shell: JsonObject {
|
||||
property string version: "0.7.7"
|
||||
property string releaseChannel: "stable"
|
||||
property string qsVersion: "0.0.0"
|
||||
}
|
||||
property JsonObject overlays: JsonObject {
|
||||
property bool enabled: true
|
||||
property bool volumeOverlayEnabled: true
|
||||
property bool brightnessOverlayEnabled: true
|
||||
property string volumeOverlayPosition: "top"
|
||||
property string brightnessOverlayPosition: "top"
|
||||
}
|
||||
property JsonObject launcher: JsonObject {
|
||||
property bool fuzzySearchEnabled: true
|
||||
property string webSearchEngine: "google"
|
||||
}
|
||||
property JsonObject bar: JsonObject {
|
||||
property string position: "top"
|
||||
property bool enabled: true
|
||||
property bool merged: false
|
||||
property bool floating: false
|
||||
property bool gothCorners: true
|
||||
property int radius: Appearance.rounding.large
|
||||
property int margins: Appearance.margin.normal
|
||||
property int density: 50
|
||||
property JsonObject modules: JsonObject {
|
||||
property color paddingColor: Appearance.m3colors.m3surfaceContainer
|
||||
property int radius: Appearance.rounding.normal
|
||||
property int height: 34
|
||||
property JsonObject workspaces: JsonObject {
|
||||
property bool enabled: true
|
||||
property int workspaceIndicators: 8
|
||||
property bool showAppIcons: true
|
||||
property bool showJapaneseNumbers: false
|
||||
}
|
||||
property JsonObject statusIcons: JsonObject {
|
||||
property bool enabled: true
|
||||
property bool networkStatusEnabled: true
|
||||
property bool bluetoothStatusEnabled: true
|
||||
}
|
||||
property JsonObject systemUsage: JsonObject {
|
||||
property bool enabled: true
|
||||
property bool cpuStatsEnabled: true
|
||||
property bool memoryStatsEnabled: true
|
||||
property bool tempStatsEnabled: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
39
.config/quickshell/nucleus-shell/config/Directories.qml
Normal file
39
.config/quickshell/nucleus-shell/config/Directories.qml
Normal file
@@ -0,0 +1,39 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.modules.functions
|
||||
import QtCore
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
// XDG Dirs, with "file://"
|
||||
readonly property string home: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
|
||||
readonly property string config: StandardPaths.standardLocations(StandardPaths.ConfigLocation)[0]
|
||||
readonly property string state: StandardPaths.standardLocations(StandardPaths.StateLocation)[0]
|
||||
readonly property string cache: StandardPaths.standardLocations(StandardPaths.CacheLocation)[0]
|
||||
readonly property string genericCache: StandardPaths.standardLocations(StandardPaths.GenericCacheLocation)[0]
|
||||
readonly property string documents: StandardPaths.standardLocations(StandardPaths.DocumentsLocation)[0]
|
||||
readonly property string downloads: StandardPaths.standardLocations(StandardPaths.DownloadLocation)[0]
|
||||
readonly property string pictures: StandardPaths.standardLocations(StandardPaths.PicturesLocation)[0]
|
||||
readonly property string music: StandardPaths.standardLocations(StandardPaths.MusicLocation)[0]
|
||||
readonly property string videos: StandardPaths.standardLocations(StandardPaths.MoviesLocation)[0]
|
||||
|
||||
property string shellConfig: FileUtils.trimFileProtocol(`${Directories.config}/nucleus-shell`)
|
||||
property string shellConfigName: "configuration.json"
|
||||
property string shellConfigPath: `${Directories.shellConfig}/config/${Directories.shellConfigName}`
|
||||
property string generatedMaterialThemePath: FileUtils.trimFileProtocol(`${Directories.config}/nucleus-shell/config/colors.json`)
|
||||
property string defaultsPath: Quickshell.shellPath("defaults")
|
||||
property string scriptsPath: Quickshell.shellPath("scripts")
|
||||
property string assetsPath: Quickshell.shellPath("assets")
|
||||
// Cleanup on init
|
||||
Component.onCompleted: {
|
||||
Quickshell.execDetached(["mkdir", "-p", `${shellConfig}`])
|
||||
Quickshell.execDetached(["mkdir", "-p", `${shellConfig}/config`])
|
||||
Quickshell.execDetached(["mkdir", "-p", `${shellConfig}/plugins`])
|
||||
Quickshell.execDetached(["mkdir", "-p", `${FileUtils.trimFileProtocol(Directories.pictures)}/Screenshots`])
|
||||
// Create dirs for intelligence shit
|
||||
Quickshell.execDetached(["mkdir", "-p", FileUtils.trimFileProtocol(`${config}/zenith/`), FileUtils.trimFileProtocol(`${config}/zenith/chats`)])
|
||||
Quickshell.execDetached(["touch", FileUtils.trimFileProtocol(`${config}/zenith/chats/default.txt`)])
|
||||
}
|
||||
}
|
||||
23
.config/quickshell/nucleus-shell/config/Globals.qml
Normal file
23
.config/quickshell/nucleus-shell/config/Globals.qml
Normal file
@@ -0,0 +1,23 @@
|
||||
import QtQuick
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
property QtObject visiblility
|
||||
property QtObject states
|
||||
|
||||
visiblility: QtObject {
|
||||
property bool powermenu: false
|
||||
property bool launcher: false
|
||||
property bool sidebarRight: false
|
||||
property bool sidebarLeft: false
|
||||
}
|
||||
|
||||
states: QtObject {
|
||||
property bool settingsOpen: false
|
||||
property bool intelligenceWindowOpen: false
|
||||
}
|
||||
|
||||
}
|
||||
97
.config/quickshell/nucleus-shell/config/Ipc.qml
Normal file
97
.config/quickshell/nucleus-shell/config/Ipc.qml
Normal file
@@ -0,0 +1,97 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.services
|
||||
import qs.modules.interface.settings
|
||||
|
||||
Scope {
|
||||
id: global
|
||||
|
||||
// Track if notification has already been shown
|
||||
property bool themeNotificationShown: false
|
||||
|
||||
// Function to show notification once
|
||||
function notifyPredefinedTheme() {
|
||||
if (!themeNotificationShown) {
|
||||
themeNotificationShown = true
|
||||
Quickshell.execDetached([
|
||||
"notify-send",
|
||||
"Nucleus Shell",
|
||||
`You are using a predefined theme ${Config.runtime.appearance.colors.scheme}. Color generation/Light Theme is not supported for this theme.`,
|
||||
"--urgency=normal",
|
||||
"--expire-time=5000"
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Global IPCs
|
||||
IpcHandler {
|
||||
target: "global"
|
||||
|
||||
function toggleTheme() {
|
||||
const currentTheme = Config.runtime.appearance.theme
|
||||
const newTheme = currentTheme === "light" ? "dark" : "light"
|
||||
|
||||
// Predefined themes: validate variant BEFORE changing theme
|
||||
if (!Config.runtime.appearance.colors.autogenerated) {
|
||||
const scheme = Config.runtime.appearance.colors.scheme
|
||||
const file = Theme.map[scheme]?.[newTheme]
|
||||
|
||||
if (!file) {
|
||||
Theme.notifyMissingVariant(scheme, newTheme)
|
||||
return
|
||||
}
|
||||
|
||||
Config.updateKey("appearance.theme", newTheme)
|
||||
|
||||
Quickshell.execDetached([
|
||||
"bash",
|
||||
Directories.scriptsPath + "/interface/switchTheme.sh",
|
||||
file
|
||||
])
|
||||
return
|
||||
}
|
||||
|
||||
// Autogenerated themes: safe to toggle freely
|
||||
Config.updateKey("appearance.theme", newTheme)
|
||||
genThemeColors.running = true
|
||||
}
|
||||
|
||||
function regenColors() {
|
||||
if (Config.runtime.appearance.colors.autogenerated) {
|
||||
genThemeColors.running = true
|
||||
} else {
|
||||
notifyPredefinedTheme()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property var genColorsCmd: Config.runtime.appearance.colors.runMatugenUserWide
|
||||
? [
|
||||
"bash",
|
||||
Directories.scriptsPath + "/interface/gencolors.sh",
|
||||
"--user-wide",
|
||||
Config.runtime.appearance.background.path,
|
||||
Config.runtime.appearance.colors.matugenScheme,
|
||||
Config.runtime.appearance.theme,
|
||||
Quickshell.shellPath("extras/matugen/config.toml")
|
||||
]
|
||||
: [
|
||||
"bash",
|
||||
Directories.scriptsPath + "/interface/gencolors.sh",
|
||||
Config.runtime.appearance.background.path,
|
||||
Config.runtime.appearance.colors.matugenScheme,
|
||||
Config.runtime.appearance.theme,
|
||||
Quickshell.shellPath("extras/matugen/config.toml")
|
||||
];
|
||||
|
||||
|
||||
|
||||
// Process to generate colors
|
||||
Process {
|
||||
id: genThemeColors
|
||||
|
||||
command: genColorsCmd
|
||||
}
|
||||
|
||||
}
|
||||
89
.config/quickshell/nucleus-shell/config/MaterialColors.qml
Normal file
89
.config/quickshell/nucleus-shell/config/MaterialColors.qml
Normal file
@@ -0,0 +1,89 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import QtCore
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
Singleton {
|
||||
id: m3colors
|
||||
property string filePath: Directories.generatedMaterialThemePath
|
||||
property alias colors: colorsJsonAdapter
|
||||
property bool ready: false
|
||||
|
||||
FileView {
|
||||
id: colorsFileView
|
||||
path: m3colors.filePath
|
||||
watchChanges: true
|
||||
onLoaded: m3colors.ready = true
|
||||
onFileChanged: colorsFileView.reload()
|
||||
onLoadFailed: error => {
|
||||
if (error === FileViewError.FileNotFound) {
|
||||
console.warn("MaterialColors: colors.json not found, writing defaults")
|
||||
writeAdapter()
|
||||
} else {
|
||||
console.error("MaterialColors: failed to load colors.json:", error)
|
||||
}
|
||||
}
|
||||
|
||||
JsonAdapter {
|
||||
id: colorsJsonAdapter
|
||||
|
||||
// === Default Matugen color scheme ===
|
||||
property string background: "#131313"
|
||||
property string error: "#ffb4ab"
|
||||
property string error_container: "#93000a"
|
||||
property string inverse_on_surface: "#303030"
|
||||
property string inverse_primary: "#00677f"
|
||||
property string inverse_surface: "#e2e2e2"
|
||||
property string on_background: "#e2e2e2"
|
||||
property string on_error: "#690005"
|
||||
property string on_error_container: "#ffdad6"
|
||||
property string on_primary: "#003543"
|
||||
property string on_primary_container: "#b7eaff"
|
||||
property string on_primary_fixed: "#001f28"
|
||||
property string on_primary_fixed_variant: "#004e60"
|
||||
property string on_secondary: "#1e333b"
|
||||
property string on_secondary_container: "#cfe6f1"
|
||||
property string on_secondary_fixed: "#071e26"
|
||||
property string on_secondary_fixed_variant: "#344a52"
|
||||
property string on_surface: "#e2e2e2"
|
||||
property string on_surface_variant: "#c6c6c6"
|
||||
property string on_tertiary: "#2c2e4d"
|
||||
property string on_tertiary_container: "#e0e0ff"
|
||||
property string on_tertiary_fixed: "#171937"
|
||||
property string on_tertiary_fixed_variant: "#424465"
|
||||
property string outline: "#919191"
|
||||
property string outline_variant: "#474747"
|
||||
property string primary: '#a571f2'
|
||||
property string primary_container: "#004e60"
|
||||
property string primary_fixed: "#b7eaff"
|
||||
property string primary_fixed_dim: "#5cd5fb"
|
||||
property string scrim: "#000000"
|
||||
property string secondary: "#b3cad4"
|
||||
property string secondary_container: "#344a52"
|
||||
property string secondary_fixed: "#cfe6f1"
|
||||
property string secondary_fixed_dim: "#b3cad4"
|
||||
property string shadow: "#000000"
|
||||
property string source_color: "#829aa4"
|
||||
property string surface: "#131313"
|
||||
property string surface_bright: "#393939"
|
||||
property string surface_container: "#1f1f1f"
|
||||
property string surface_container_high: "#2a2a2a"
|
||||
property string surface_container_highest: "#353535"
|
||||
property string surface_container_low: "#1b1b1b"
|
||||
property string surface_container_lowest: "#0e0e0e"
|
||||
property string surface_dim: "#131313"
|
||||
property string surface_tint: "#5cd5fb"
|
||||
property string surface_variant: "#474747"
|
||||
property string tertiary: "#c3c3eb"
|
||||
property string tertiary_container: "#424465"
|
||||
property string tertiary_fixed: "#e0e0ff"
|
||||
property string tertiary_fixed_dim: "#c3c3eb"
|
||||
}
|
||||
}
|
||||
|
||||
function reload() {
|
||||
colorsFileView.reload()
|
||||
}
|
||||
}
|
||||
133
.config/quickshell/nucleus-shell/config/Metrics.qml
Normal file
133
.config/quickshell/nucleus-shell/config/Metrics.qml
Normal file
@@ -0,0 +1,133 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property double durationScale: Config.runtime.appearance.animations.durationScale
|
||||
readonly property double roundingScale: Config.runtime.appearance.rounding.factor
|
||||
readonly property double fontScale: Config.runtime.appearance.font.scale
|
||||
|
||||
function spacing(value) { // These will be used with a scale later on...
|
||||
return value
|
||||
}
|
||||
|
||||
function padding(value) {
|
||||
return value
|
||||
}
|
||||
|
||||
function chronoDuration(value) {
|
||||
if (typeof value === "number")
|
||||
return value * durationScale
|
||||
|
||||
switch (value) {
|
||||
case "supershort": return Appearance.animation.durations.supershort * durationScale
|
||||
case "small": return Appearance.animation.durations.small * durationScale
|
||||
case "normal": return Appearance.animation.durations.normal * durationScale
|
||||
case "large": return Appearance.animation.durations.large * durationScale
|
||||
case "extraLarge": return Appearance.animation.durations.extraLarge * durationScale
|
||||
case "expressiveFastSpatial": return Appearance.animation.durations.expressiveFastSpatial * durationScale
|
||||
case "expressiveDefaultSpatial": return Appearance.animation.durations.expressiveDefaultSpatial * durationScale
|
||||
case "expressiveEffects": return Appearance.animation.durations.expressiveEffects * durationScale
|
||||
default: return 0
|
||||
}
|
||||
}
|
||||
|
||||
function margin(value) {
|
||||
if (typeof value === "number")
|
||||
return value
|
||||
|
||||
switch (value) {
|
||||
case "supertiny": return Appearance.margin.supertiny
|
||||
case "tinier": return Appearance.margin.tinier
|
||||
case "tiny": return Appearance.margin.tiny
|
||||
case "verysmall": return Appearance.margin.verysmall
|
||||
case "small": return Appearance.margin.small
|
||||
case "normal": return Appearance.margin.normal
|
||||
case "large": return Appearance.margin.large
|
||||
case "verylarge": return Appearance.margin.verylarge
|
||||
case "extraLarge": return Appearance.margin.extraLarge
|
||||
default: return 0
|
||||
}
|
||||
}
|
||||
|
||||
function radius(value) {
|
||||
if (typeof value === "number")
|
||||
return value * roundingScale
|
||||
|
||||
switch (value) {
|
||||
case "unsharpen": return Appearance.rounding.unsharpen * roundingScale
|
||||
case "unsharpenmore": return Appearance.rounding.unsharpenmore * roundingScale
|
||||
case "verysmall": return Appearance.rounding.verysmall * roundingScale
|
||||
case "small": return Appearance.rounding.small * roundingScale
|
||||
case "normal": return Appearance.rounding.normal * roundingScale
|
||||
case "large": return Appearance.rounding.large * roundingScale
|
||||
case "verylarge": return Appearance.rounding.verylarge * roundingScale
|
||||
case "childish": return Appearance.rounding.childish * roundingScale
|
||||
case "full": return Appearance.rounding.full * roundingScale
|
||||
case "screenRounding": return Appearance.rounding.screenRounding * roundingScale
|
||||
case "windowRounding": return Appearance.rounding.windowRounding * roundingScale
|
||||
default: return 0
|
||||
}
|
||||
}
|
||||
|
||||
function fontSize(value) {
|
||||
if (typeof value === "number")
|
||||
return value * fontScale
|
||||
|
||||
switch (value) {
|
||||
case "smallest": return Appearance.font.size.smallest * fontScale
|
||||
case "smaller": return Appearance.font.size.smaller * fontScale
|
||||
case "smallie": return Appearance.font.size.smallie * fontScale
|
||||
case "small": return Appearance.font.size.small * fontScale
|
||||
case "normal": return Appearance.font.size.normal * fontScale
|
||||
case "large": return Appearance.font.size.large * fontScale
|
||||
case "larger": return Appearance.font.size.larger * fontScale
|
||||
case "big": return Appearance.font.size.big * fontScale
|
||||
case "huge": return Appearance.font.size.huge * fontScale
|
||||
case "hugeass": return Appearance.font.size.hugeass * fontScale
|
||||
case "wildass": return Appearance.font.size.wildass * fontScale
|
||||
case "title": return Appearance.font.size.title * fontScale
|
||||
default: return Appearance.font.size.normal * fontScale
|
||||
}
|
||||
}
|
||||
|
||||
function iconSize(value) {
|
||||
if (typeof value === "number")
|
||||
return value * fontScale
|
||||
|
||||
switch (value) {
|
||||
case "smallest": return Appearance.font.size.icon.smallest * fontScale
|
||||
case "smaller": return Appearance.font.size.icon.smaller * fontScale
|
||||
case "smallie": return Appearance.font.size.icon.smallie * fontScale
|
||||
case "small": return Appearance.font.size.icon.small * fontScale
|
||||
case "normal": return Appearance.font.size.icon.normal * fontScale
|
||||
case "large": return Appearance.font.size.icon.large * fontScale
|
||||
case "larger": return Appearance.font.size.icon.larger * fontScale
|
||||
case "big": return Appearance.font.size.icon.big * fontScale
|
||||
case "huge": return Appearance.font.size.icon.huge * fontScale
|
||||
case "hugeass": return Appearance.font.size.icon.hugeass * fontScale
|
||||
case "wildass": return Appearance.font.size.icon.wildass * fontScale
|
||||
case "title": return Appearance.font.size.icon.title * fontScale
|
||||
default: return Appearance.font.size.icon.normal * fontScale
|
||||
}
|
||||
}
|
||||
|
||||
function fontFamily(value) {
|
||||
if (typeof value === "string") {
|
||||
switch (value) {
|
||||
case "main": return Appearance.font.family.main
|
||||
case "title": return Appearance.font.family.title
|
||||
case "materialIcons": return Appearance.font.family.materialIcons
|
||||
case "nerdIcons": return Appearance.font.family.nerdIcons
|
||||
case "monospace": return Appearance.font.family.monospace
|
||||
case "reading": return Appearance.font.family.reading
|
||||
case "expressive": return Appearance.font.family.expressive
|
||||
default: return value
|
||||
}
|
||||
}
|
||||
return Appearance.font.family.main
|
||||
}
|
||||
}
|
||||
BIN
.config/quickshell/nucleus-shell/defaults/default.jpg
Normal file
BIN
.config/quickshell/nucleus-shell/defaults/default.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 MiB |
@@ -0,0 +1,6 @@
|
||||
[config]
|
||||
reload = false
|
||||
|
||||
[templates.nucleus]
|
||||
input_path = './templates/colors.json'
|
||||
output_path = '~/.config/nucleus-shell/config/colors.json'
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"_comment": "Material Colors generated with Matugen"
|
||||
<* for name, value in colors *>
|
||||
, "{{ name }}": "{{ value.default.hex }}"
|
||||
<* endfor *>
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.config
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property real value: 0.65 // 0.0 → 1.0
|
||||
property real strokeWidth: 2
|
||||
property color bgColor: Appearance.m3colors.m3secondaryContainer
|
||||
property color fgColor: Appearance.m3colors.m3primary
|
||||
property string icon: "battery_full"
|
||||
property int iconSize: Metrics.iconSize(20)
|
||||
property bool fillIcon: false
|
||||
|
||||
width: 22
|
||||
height: 24
|
||||
onValueChanged: canvas.requestPaint()
|
||||
|
||||
Canvas {
|
||||
id: canvas
|
||||
|
||||
anchors.fill: parent
|
||||
onPaint: {
|
||||
const ctx = getContext("2d");
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
const cx = width / 2;
|
||||
const cy = height / 2;
|
||||
const r = (width - root.strokeWidth) / 2;
|
||||
const start = -Math.PI / 2;
|
||||
const end = start + 2 * Math.PI * root.value;
|
||||
ctx.lineWidth = root.strokeWidth;
|
||||
ctx.lineCap = "round";
|
||||
// background ring
|
||||
ctx.strokeStyle = root.bgColor;
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, r, 0, 2 * Math.PI);
|
||||
ctx.stroke();
|
||||
// progress ring
|
||||
ctx.strokeStyle = root.fgColor;
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, r, start, end);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
// CENTER ICON
|
||||
MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
icon: root.icon
|
||||
iconSize: root.iconSize
|
||||
font.variableAxes: {
|
||||
"FILL": root.fillIcon ? 1 : 0
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: contentCard
|
||||
implicitWidth: parent ? parent.width : 600
|
||||
implicitHeight: contentArea.implicitHeight + verticalPadding
|
||||
|
||||
default property alias content: contentArea.data
|
||||
property alias color: bg.color
|
||||
property alias radius: bg.radius
|
||||
property int cardMargin: Metrics.margin("normal")
|
||||
property int cardSpacing: Metrics.margin("small")
|
||||
property int verticalPadding: Metrics.margin("verylarge")
|
||||
property bool useAnims: true
|
||||
|
||||
Rectangle {
|
||||
id: bg
|
||||
anchors.fill: parent
|
||||
radius: Metrics.radius("normal")
|
||||
color: Appearance.colors.colLayer1
|
||||
|
||||
Behavior on color {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
ColorAnimation {
|
||||
duration: !contentCard.useAnims ? 0 : Metrics.chronoDuration("fast")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: contentArea
|
||||
anchors.fill: parent
|
||||
anchors.margins: cardMargin
|
||||
spacing: cardSpacing
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: contentMenu
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
opacity: visible ? 1 : 0
|
||||
scale: visible ? 1 : 0.95
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
NumberAnimation {
|
||||
duration: Metrics.chronoDuration("normal")
|
||||
easing.type: Appearance.animation.curves.standard[0] // using standard easing
|
||||
}
|
||||
}
|
||||
Behavior on scale {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
NumberAnimation {
|
||||
duration: Metrics.chronoDuration("normal")
|
||||
easing.type: Appearance.animation.curves.standard[0]
|
||||
}
|
||||
}
|
||||
|
||||
property string title: ""
|
||||
property string description: ""
|
||||
default property alias content: stackedSections.data
|
||||
|
||||
Item {
|
||||
id: headerArea
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: Metrics.margin("verylarge")
|
||||
anchors.leftMargin: Metrics.margin("verylarge")
|
||||
anchors.rightMargin: Metrics.margin("verylarge")
|
||||
width: parent.width
|
||||
|
||||
ColumnLayout {
|
||||
id: headerContent
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: Metrics.margin("small")
|
||||
|
||||
ColumnLayout {
|
||||
StyledText {
|
||||
text: contentMenu.title
|
||||
font.pixelSize: Metrics.fontSize("huge")
|
||||
font.bold: true
|
||||
font.family: Metrics.fontFamily("title")
|
||||
}
|
||||
StyledText {
|
||||
text: contentMenu.description
|
||||
font.pixelSize: Metrics.fontSize("small")
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: hr
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignRight
|
||||
implicitHeight: 1
|
||||
}
|
||||
}
|
||||
|
||||
height: headerContent.implicitHeight
|
||||
}
|
||||
|
||||
Flickable {
|
||||
id: mainScroll
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: headerArea.bottom
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.leftMargin: Metrics.margin("verylarge")
|
||||
anchors.rightMargin: Metrics.margin("verylarge")
|
||||
anchors.topMargin: Metrics.margin("normal")
|
||||
clip: true
|
||||
interactive: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
|
||||
contentHeight: mainContent.childrenRect.height + Appearance.margin.small
|
||||
contentWidth: width
|
||||
|
||||
Item {
|
||||
id: mainContent
|
||||
width: mainScroll.width
|
||||
height: mainContent.childrenRect.height
|
||||
|
||||
Column {
|
||||
id: stackedSections
|
||||
width: Math.min(mainScroll.width, 1000)
|
||||
x: (mainContent.width - width) / 2
|
||||
spacing: Appearance.margin.normal
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: baseCard
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
implicitHeight: wpBG.implicitHeight
|
||||
|
||||
default property alias content: contentArea.data
|
||||
property alias color: wpBG.color
|
||||
|
||||
property int cardMargin: Metrics.margin(20)
|
||||
property int cardSpacing: Metrics.spacing(10)
|
||||
property int radius: Metrics.radius("large")
|
||||
property int verticalPadding: Metrics.padding(40)
|
||||
|
||||
Rectangle {
|
||||
id: wpBG
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
implicitHeight: contentArea.implicitHeight + baseCard.verticalPadding
|
||||
Behavior on implicitHeight {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
NumberAnimation {
|
||||
duration: Metrics.chronoDuration("small")
|
||||
easing.type: Easing.InOutExpo
|
||||
}
|
||||
}
|
||||
color: Appearance.m3colors.m3surfaceContainerLow
|
||||
Behavior on color {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
ColorAnimation {
|
||||
duration: Metrics.chronoDuration("small")
|
||||
easing.type: Easing.InOutExpo
|
||||
}
|
||||
}
|
||||
radius: baseCard.radius
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: contentArea
|
||||
anchors.top: wpBG.top
|
||||
anchors.left: wpBG.left
|
||||
anchors.right: wpBG.right
|
||||
anchors.margins: baseCard.cardMargin
|
||||
spacing: baseCard.cardSpacing
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
ContentRowCard {
|
||||
id: infoCard
|
||||
|
||||
// --- Properties ---
|
||||
property string icon: "info"
|
||||
property color backgroundColor: Appearance.m3colors.darkMode ? Qt.lighter(Appearance.m3colors.m3error, 3.5) : Qt.lighter(Appearance.m3colors.m3error, 1)
|
||||
property color contentColor: Appearance.m3colors.m3onPrimary
|
||||
property string title: "Title"
|
||||
property string description: "Description"
|
||||
|
||||
color: backgroundColor
|
||||
cardSpacing: Metrics.spacing(12) // nice spacing between elements
|
||||
|
||||
RowLayout {
|
||||
id: mainLayout
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
spacing: Metrics.spacing(16)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
// --- Icon ---
|
||||
MaterialSymbol {
|
||||
id: infoIcon
|
||||
icon: infoCard.icon
|
||||
iconSize: Metrics.iconSize(26)
|
||||
color: contentColor
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
// --- Text column ---
|
||||
ColumnLayout {
|
||||
spacing: Metrics.spacing(2)
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
StyledText {
|
||||
text: infoCard.title
|
||||
font.bold: true
|
||||
color: contentColor
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: infoCard.description
|
||||
color: contentColor
|
||||
font.pixelSize: Metrics.fontSize(12)
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import QtQuick
|
||||
import qs.config
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property alias icon: mIcon.icon
|
||||
property real size: Metrics.iconSize(28)
|
||||
width: size
|
||||
height: size
|
||||
MaterialSymbol {
|
||||
id: mIcon
|
||||
anchors.centerIn: parent
|
||||
icon: "progress_activity"
|
||||
font.pixelSize: root.size
|
||||
color: Appearance.m3colors.m3primary
|
||||
renderType: Text.QtRendering
|
||||
}
|
||||
RotationAnimator on rotation {
|
||||
target: mIcon
|
||||
running: true
|
||||
loops: Animation.Infinite
|
||||
from: 0
|
||||
to: 360
|
||||
duration: Metrics.chronoDuration(1000)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import QtQuick
|
||||
import qs.config
|
||||
|
||||
StyledText {
|
||||
property string icon: ""
|
||||
property int fill: 0
|
||||
property int iconSize: Metrics.iconSize("large")
|
||||
|
||||
font.family: Appearance.font.family.materialIcons
|
||||
font.pixelSize: iconSize
|
||||
text: icon
|
||||
font.variableAxes: {
|
||||
"FILL": fill
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.config
|
||||
|
||||
MaterialSymbol {
|
||||
id: root
|
||||
|
||||
// Expose mouse props
|
||||
property alias enabled: ma.enabled
|
||||
property alias hoverEnabled: ma.hoverEnabled
|
||||
property alias pressed: ma.pressed
|
||||
property string tooltipText: ""
|
||||
|
||||
// Renamed signals (no collisions possible)
|
||||
signal buttonClicked()
|
||||
signal buttonEntered()
|
||||
signal buttonExited()
|
||||
signal buttonPressAndHold()
|
||||
signal buttonPressedChanged(bool pressed)
|
||||
|
||||
MouseArea {
|
||||
id: ma
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: root.buttonClicked()
|
||||
onEntered: root.buttonEntered()
|
||||
onExited: root.buttonExited()
|
||||
onPressAndHold: root.buttonPressAndHold()
|
||||
onPressedChanged: root.buttonPressedChanged(pressed)
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: hover
|
||||
|
||||
enabled: root.tooltipText !== ""
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
active: root.tooltipText !== ""
|
||||
StyledPopout {
|
||||
hoverTarget: hover
|
||||
hoverDelay: Metrics.chronoDuration(500)
|
||||
|
||||
Component {
|
||||
StyledText {
|
||||
text: root.tooltipText
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.config
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
property string label: ""
|
||||
property string description: ""
|
||||
property string prefField: ""
|
||||
property double step: 1.0
|
||||
property double minimum: -2.14748e+09 // Largest num I could find and type ig
|
||||
property double maximum: 2.14748e+09
|
||||
|
||||
// Floating-point value
|
||||
property double value: readValue()
|
||||
|
||||
function readValue() {
|
||||
if (!prefField)
|
||||
return 0;
|
||||
|
||||
var parts = prefField.split('.');
|
||||
var cur = Config.runtime;
|
||||
|
||||
for (var i = 0; i < parts.length; ++i) {
|
||||
if (cur === undefined || cur === null)
|
||||
return 0;
|
||||
cur = cur[parts[i]];
|
||||
}
|
||||
|
||||
var n = Number(cur);
|
||||
return isNaN(n) ? 0 : n;
|
||||
}
|
||||
|
||||
function writeValue(v) {
|
||||
if (!prefField)
|
||||
return;
|
||||
|
||||
var nv = Math.max(minimum, Math.min(maximum, v));
|
||||
nv = Number(nv.toFixed(2)); // precision control (adjust if needed)
|
||||
Config.updateKey(prefField, nv);
|
||||
}
|
||||
|
||||
spacing: Metrics.spacing(8)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Metrics.spacing(2)
|
||||
|
||||
StyledText {
|
||||
text: root.label
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.description
|
||||
font.pixelSize: Metrics.fontSize(10)
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
RowLayout {
|
||||
spacing: Metrics.spacing(6)
|
||||
|
||||
StyledButton {
|
||||
text: "-"
|
||||
implicitWidth: 36
|
||||
onClicked: writeValue(readValue() - step)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: value.toFixed(2)
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
width: 72
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
text: "+"
|
||||
implicitWidth: 36
|
||||
onClicked: writeValue(readValue() + step)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.config
|
||||
import qs.modules.functions
|
||||
|
||||
Control {
|
||||
id: root
|
||||
|
||||
property alias text: label.text
|
||||
property string icon: ""
|
||||
property int iconSize: Metrics.iconSize(20)
|
||||
property alias radius: background.radius
|
||||
property alias topLeftRadius: background.topLeftRadius
|
||||
property alias topRightRadius: background.topRightRadius
|
||||
property alias bottomLeftRadius: background.bottomLeftRadius
|
||||
property alias bottomRightRadius: background.bottomRightRadius
|
||||
property bool checkable: false
|
||||
property bool checked: true
|
||||
property bool secondary: false
|
||||
property string tooltipText: ""
|
||||
property bool usePrimary: secondary ? false : checked
|
||||
property color base_bg: usePrimary ? Appearance.m3colors.m3primary : Appearance.m3colors.m3secondaryContainer
|
||||
property color base_fg: usePrimary ? Appearance.m3colors.m3onPrimary : Appearance.m3colors.m3onSecondaryContainer
|
||||
property color disabled_bg: ColorUtils.transparentize(base_bg, 0.4)
|
||||
property color disabled_fg: ColorUtils.transparentize(base_fg, 0.4)
|
||||
property color hover_bg: Qt.lighter(base_bg, 1.1)
|
||||
property color pressed_bg: Qt.darker(base_bg, 1.2)
|
||||
property color backgroundColor: !root.enabled ? disabled_bg : mouse_area.pressed ? pressed_bg : mouse_area.containsMouse ? hover_bg : base_bg
|
||||
property color textColor: !root.enabled ? disabled_fg : base_fg
|
||||
property bool beingHovered: mouse_area.containsMouse
|
||||
|
||||
signal clicked()
|
||||
signal toggled(bool checked)
|
||||
|
||||
implicitWidth: (label.text === "" && icon !== "") ? implicitHeight : row.implicitWidth + implicitHeight
|
||||
implicitHeight: 40
|
||||
|
||||
MouseArea {
|
||||
id: mouse_area
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: root.enabled
|
||||
cursorShape: root.enabled ? Qt.PointingHandCursor : Qt.ForbiddenCursor
|
||||
onClicked: {
|
||||
if (!root.enabled)
|
||||
return ;
|
||||
|
||||
if (root.checkable) {
|
||||
root.checked = !root.checked;
|
||||
root.toggled(root.checked);
|
||||
}
|
||||
root.clicked();
|
||||
}
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: hover
|
||||
|
||||
enabled: root.tooltipText !== ""
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
active: root.tooltipText !== ""
|
||||
|
||||
StyledPopout {
|
||||
hoverTarget: hover
|
||||
hoverDelay: Metrics.chronoDuration(500)
|
||||
|
||||
Component {
|
||||
StyledText {
|
||||
text: root.tooltipText
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
contentItem: Item {
|
||||
anchors.fill: parent
|
||||
|
||||
Row {
|
||||
id: row
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: root.icon !== "" && label.text !== "" ? 5 : 0
|
||||
|
||||
MaterialSymbol {
|
||||
visible: root.icon !== ""
|
||||
icon: root.icon
|
||||
font.pixelSize: root.iconSize
|
||||
color: root.textColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Metrics.chronoDuration("small") / 2
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: label
|
||||
|
||||
color: root.textColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
elide: Text.ElideRight
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Metrics.chronoDuration("small") / 2
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
id: background
|
||||
|
||||
radius: Metrics.radius("large")
|
||||
color: root.backgroundColor
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Metrics.chronoDuration("small") / 2
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on radius {
|
||||
NumberAnimation {
|
||||
duration: Metrics.chronoDuration("small") / 2
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on topLeftRadius {
|
||||
NumberAnimation {
|
||||
duration: Metrics.chronoDuration("small")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on topRightRadius {
|
||||
NumberAnimation {
|
||||
duration: Metrics.chronoDuration("small")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on bottomLeftRadius {
|
||||
NumberAnimation {
|
||||
duration: Metrics.chronoDuration("small")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on bottomRightRadius {
|
||||
NumberAnimation {
|
||||
duration: Metrics.chronoDuration("small")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Effects
|
||||
import qs.config
|
||||
import qs.modules.functions
|
||||
|
||||
Item {
|
||||
id: root
|
||||
width: 200
|
||||
height: 56
|
||||
|
||||
property string label: "Select option"
|
||||
property var model: ["Option 1", "Option 2", "Option 3", "Option 4", "Option 5"]
|
||||
property int currentIndex: -1
|
||||
property string currentText: {
|
||||
if (currentIndex < 0)
|
||||
return ""
|
||||
|
||||
if (textRole && model && model.get)
|
||||
return model.get(currentIndex)[textRole] ?? ""
|
||||
|
||||
return model[currentIndex] ?? ""
|
||||
}
|
||||
property bool enabled: true
|
||||
property string textRole: ""
|
||||
|
||||
signal selectedIndexChanged(int index)
|
||||
|
||||
Rectangle {
|
||||
id: container
|
||||
anchors.fill: parent
|
||||
color: "transparent"
|
||||
border.color: dropdown.activeFocus ? Appearance.m3colors.m3primary : Appearance.m3colors.m3outline
|
||||
border.width: dropdown.activeFocus ? 2 : 1
|
||||
radius: Metrics.radius("unsharpen")
|
||||
|
||||
Behavior on border.color {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
ColorAnimation { duration: Metrics.chronoDuration("small") ; easing.type: Easing.InOutCubic }
|
||||
}
|
||||
Behavior on border.width {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
NumberAnimation { duration: Metrics.chronoDuration("small") ; easing.type: Easing.InOutCubic }
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
enabled: root.enabled
|
||||
hoverEnabled: true
|
||||
onClicked: dropdown.popup.visible ? dropdown.popup.close() : dropdown.popup.open()
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: parent.parent.radius
|
||||
color: Appearance.m3colors.m3primary
|
||||
opacity: mouseArea.pressed ? 0.12 : mouseArea.containsMouse ? 0.08 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
NumberAnimation { duration: Metrics.chronoDuration("small") ; easing.type: Easing.InOutCubic }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Metrics.margin(16)
|
||||
anchors.rightMargin: Metrics.margin(12)
|
||||
spacing: Metrics.spacing(12)
|
||||
|
||||
StyledText {
|
||||
id: labelText
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
text: root.currentIndex >= 0 ? root.currentText : root.label
|
||||
color: root.currentIndex >= 0
|
||||
? Appearance.m3colors.m3onSurface
|
||||
: ColorUtils.transparentize(Appearance.m3colors.m3onSurfaceVariant, 0.7)
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
elide: Text.ElideRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
MaterialSymbol {
|
||||
id: dropdownIcon
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
icon: dropdown.popup.visible ? "arrow_drop_up" : "arrow_drop_down"
|
||||
iconSize: Metrics.iconSize(20)
|
||||
color: Appearance.m3colors.m3onSurfaceVariant
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
id: dropdown
|
||||
visible: false
|
||||
model: root.model
|
||||
currentIndex: root.currentIndex >= 0 ? root.currentIndex : -1
|
||||
enabled: root.enabled
|
||||
textRole: root.textRole
|
||||
|
||||
onCurrentIndexChanged: {
|
||||
if (currentIndex >= 0) {
|
||||
root.currentIndex = currentIndex
|
||||
root.selectedIndexChanged(currentIndex)
|
||||
}
|
||||
}
|
||||
|
||||
popup: Popup {
|
||||
y: root.height + 4
|
||||
width: root.width
|
||||
padding: 0
|
||||
|
||||
background: Rectangle {
|
||||
color: Appearance.m3colors.m3surfaceContainer
|
||||
radius: Metrics.radius(4)
|
||||
border.color: Appearance.m3colors.m3outline
|
||||
border.width: 1
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
shadowEnabled: true
|
||||
shadowColor: ColorUtils.transparentize(Appearance.m3colors.m3shadow, 0.25)
|
||||
shadowBlur: 0.4
|
||||
shadowVerticalOffset: 8
|
||||
shadowHorizontalOffset: 0
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: ListView {
|
||||
id: listView
|
||||
clip: true
|
||||
implicitHeight: Math.min(contentHeight, 300)
|
||||
model: dropdown.popup.visible ? dropdown.model : []
|
||||
currentIndex: Math.max(0, dropdown.currentIndex)
|
||||
|
||||
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
|
||||
|
||||
delegate: ItemDelegate {
|
||||
width: listView.width
|
||||
height: 48
|
||||
|
||||
background: Rectangle {
|
||||
color: {
|
||||
if (itemMouse.pressed) return ColorUtils.transparentize(Appearance.m3colors.m3primaryContainer, 0.12)
|
||||
if (itemMouse.containsMouse) return ColorUtils.transparentize(Appearance.m3colors.m3primaryContainer, 0.08)
|
||||
if (index === root.currentIndex) return ColorUtils.transparentize(Appearance.m3colors.m3primaryContainer, 0.08)
|
||||
return "transparent"
|
||||
}
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Metrics.chronoDuration("small") ; easing.type: Easing.InOutCubic }
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: StyledText {
|
||||
text: modelData
|
||||
color: index === root.currentIndex ? Appearance.m3colors.m3primary : Appearance.m3colors.m3onSurface
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
leftPadding: Metrics.fontSize(16)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: itemMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: {
|
||||
dropdown.currentIndex = index
|
||||
dropdown.popup.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enter: Transition {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
NumberAnimation {property: "opacity"; from: 0.0; to: 1.0; duration: Metrics.chronoDuration("small") ; easing.type: Easing.InOutCubic }
|
||||
NumberAnimation {property: "scale"; from: 0.9; to: 1.0; duration: Metrics.chronoDuration("small") ; easing.type: Easing.InOutCubic }
|
||||
}
|
||||
|
||||
exit: Transition {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
NumberAnimation {property: "opacity"; from: 1.0; to: 0.0; duration: Metrics.chronoDuration(Appearance.animation.fast * 0.67); easing.type: Easing.InOutCubic }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
focus: true
|
||||
Keys.onPressed: (event) => {
|
||||
if (event.key === Qt.Key_Space || event.key === Qt.Key_Return) {
|
||||
dropdown.popup.visible ? dropdown.popup.close() : dropdown.popup.open()
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import QtQuick
|
||||
import qs.services
|
||||
import qs.config
|
||||
|
||||
Image {
|
||||
asynchronous: true
|
||||
retainWhileLoading: true
|
||||
visible: opacity > 0
|
||||
opacity: (status === Image.Ready) ? 1 : 0
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Wayland
|
||||
|
||||
LazyLoader {
|
||||
id: root
|
||||
|
||||
property string displayName: screen?.name ?? ""
|
||||
property PanelWindow instance: null
|
||||
property HoverHandler hoverTarget
|
||||
property real margin: Metrics.margin(10)
|
||||
default property list<Component> content
|
||||
property bool startAnim: false
|
||||
property bool isVisible: false
|
||||
property bool keepAlive: false
|
||||
property bool interactable: false
|
||||
property bool hasHitbox: true
|
||||
property bool hCenterOnItem: false
|
||||
property bool followMouse: false
|
||||
property list<StyledPopout> childPopouts: []
|
||||
|
||||
property bool requiresHover: true
|
||||
property bool _manualControl: false
|
||||
property int hoverDelay: Metrics.chronoDuration(250)
|
||||
|
||||
property bool targetHovered: hoverTarget && hoverTarget.hovered
|
||||
property bool containerHovered: interactable && root.item && root.item.containerHovered
|
||||
property bool selfHovered: targetHovered || containerHovered
|
||||
|
||||
property bool childrenHovered: {
|
||||
for (let i = 0; i < childPopouts.length; i++) {
|
||||
if (childPopouts[i].selfHovered)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
property bool hoverActive: selfHovered || childrenHovered
|
||||
|
||||
property Timer showDelayTimer: Timer {
|
||||
interval: root.hoverDelay
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
root.keepAlive = true;
|
||||
root.isVisible = true;
|
||||
root.startAnim = true;
|
||||
}
|
||||
}
|
||||
|
||||
property Timer hangTimer: Timer {
|
||||
interval: Metrics.chronoDuration(200)
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
root.startAnim = false;
|
||||
cleanupTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
property Timer cleanupTimer: Timer {
|
||||
interval: Metrics.chronoDuration("small")
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
root.isVisible = false;
|
||||
root.keepAlive = false;
|
||||
root._manualControl = false;
|
||||
root.instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
onHoverActiveChanged: {
|
||||
if (_manualControl)
|
||||
return;
|
||||
if (!requiresHover)
|
||||
return;
|
||||
if (hoverActive) {
|
||||
hangTimer.stop();
|
||||
cleanupTimer.stop();
|
||||
if (hoverDelay > 0) {
|
||||
showDelayTimer.restart();
|
||||
} else {
|
||||
root.keepAlive = true;
|
||||
root.isVisible = true;
|
||||
root.startAnim = true;
|
||||
}
|
||||
} else {
|
||||
showDelayTimer.stop();
|
||||
hangTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
function show() {
|
||||
hangTimer.stop();
|
||||
cleanupTimer.stop();
|
||||
showDelayTimer.stop();
|
||||
_manualControl = true;
|
||||
keepAlive = true;
|
||||
isVisible = true;
|
||||
startAnim = true;
|
||||
}
|
||||
|
||||
function hide() {
|
||||
_manualControl = true;
|
||||
showDelayTimer.stop();
|
||||
startAnim = false;
|
||||
hangTimer.stop();
|
||||
cleanupTimer.restart();
|
||||
}
|
||||
|
||||
active: keepAlive
|
||||
|
||||
component: PanelWindow {
|
||||
id: popoutWindow
|
||||
|
||||
color: "transparent"
|
||||
visible: root.isVisible
|
||||
|
||||
WlrLayershell.namespace: "whisker:popout"
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
exclusiveZone: 0
|
||||
|
||||
anchors {
|
||||
left: true
|
||||
top: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
property bool exceedingHalf: false
|
||||
property var parentPopoutWindow: null
|
||||
property point mousePos: Qt.point(0, 0)
|
||||
property bool containerHovered: root.interactable && containerHoverHandler.hovered
|
||||
|
||||
HoverHandler {
|
||||
id: windowHover
|
||||
onPointChanged: point => {
|
||||
if (root.followMouse)
|
||||
popoutWindow.mousePos = point.position;
|
||||
}
|
||||
}
|
||||
|
||||
mask: Region {
|
||||
x: !root.hasHitbox ? 0 : !requiresHover ? 0 : container.x
|
||||
y: !root.hasHitbox ? 0 : !requiresHover ? 0 : container.y
|
||||
width: !root.hasHitbox ? 0 : !requiresHover ? popoutWindow.width : container.implicitWidth
|
||||
height: !root.hasHitbox ? 0 : !requiresHover ? popoutWindow.height : container.implicitHeight
|
||||
}
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
hoverEnabled: false
|
||||
|
||||
onPressed: mouse => {
|
||||
if (!containerHoverHandler.containsMouse && root.isVisible) {
|
||||
root.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
Item {
|
||||
id: container
|
||||
|
||||
implicitWidth: contentArea.implicitWidth + root.margin * 2
|
||||
implicitHeight: contentArea.implicitHeight + root.margin * 2
|
||||
|
||||
x: {
|
||||
let xValue;
|
||||
|
||||
if (root.followMouse)
|
||||
xValue = mousePos.x + 10;
|
||||
else {
|
||||
let targetItem = hoverTarget?.parent;
|
||||
if (!targetItem)
|
||||
xValue = 0;
|
||||
else {
|
||||
let baseX = targetItem.mapToGlobal(Qt.point(0, 0)).x;
|
||||
if (parentPopoutWindow)
|
||||
baseX += parentPopoutWindow.x;
|
||||
|
||||
let targetWidth = targetItem.width;
|
||||
let popupWidth = container.implicitWidth;
|
||||
|
||||
if (root.hCenterOnItem) {
|
||||
let centeredX = baseX + (targetWidth - popupWidth) / 2;
|
||||
if (centeredX + popupWidth > screen.width)
|
||||
centeredX = screen.width - popupWidth - 10;
|
||||
if (centeredX < 10)
|
||||
centeredX = 10;
|
||||
xValue = centeredX;
|
||||
} else {
|
||||
let xPos = baseX - ((ConfigResolver.bar(root.displayName).position === "top" || ConfigResolver.bar(root.displayName).position === "top") ? 20 : -40);
|
||||
if (xPos + popupWidth > screen.width) {
|
||||
exceedingHalf = true;
|
||||
xValue = baseX - popupWidth;
|
||||
} else {
|
||||
exceedingHalf = false;
|
||||
xValue = xPos;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return root.cleanupTimer.running ? xValue : Math.round(xValue);
|
||||
}
|
||||
|
||||
y: {
|
||||
let yValue;
|
||||
|
||||
if (root.followMouse)
|
||||
yValue = mousePos.y + 10;
|
||||
else {
|
||||
let targetItem = hoverTarget?.parent;
|
||||
if (!targetItem)
|
||||
yValue = 0;
|
||||
else {
|
||||
let baseY = targetItem.mapToGlobal(Qt.point(0, 0)).y;
|
||||
if (parentPopoutWindow)
|
||||
baseY += parentPopoutWindow.y;
|
||||
|
||||
let targetHeight = targetItem.height;
|
||||
let popupHeight = container.implicitHeight;
|
||||
|
||||
let yPos = baseY + ((ConfigResolver.bar(root.displayName).position === "top" || ConfigResolver.bar(root.displayName).position === "top") ? targetHeight : 0);
|
||||
|
||||
if (yPos > screen.height / 2)
|
||||
yPos = baseY - popupHeight;
|
||||
|
||||
if (yPos + popupHeight > screen.height)
|
||||
yPos = screen.height - popupHeight - 10;
|
||||
if (yPos < 10)
|
||||
yPos = 10;
|
||||
|
||||
yValue = yPos;
|
||||
}
|
||||
}
|
||||
|
||||
return root.cleanupTimer.running ? yValue : Math.round(yValue);
|
||||
}
|
||||
|
||||
|
||||
|
||||
opacity: root.startAnim ? 1 : 0
|
||||
scale: root.interactable ? 1 : root.startAnim ? 1 : 0.9
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
shadowEnabled: true
|
||||
shadowOpacity: 1
|
||||
shadowColor: Appearance.m3colors.m3shadow
|
||||
shadowBlur: 1
|
||||
shadowScale: 1
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Metrics.chronoDuration("small")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
}
|
||||
Behavior on scale {
|
||||
enabled: !root.interactable
|
||||
NumberAnimation {
|
||||
duration: Metrics.chronoDuration("small")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
}
|
||||
Behavior on implicitWidth {
|
||||
enabled: root.interactable
|
||||
NumberAnimation {
|
||||
duration: Metrics.chronoDuration("small")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
}
|
||||
Behavior on implicitHeight {
|
||||
enabled: root.interactable
|
||||
NumberAnimation {
|
||||
duration: Metrics.chronoDuration("small")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
}
|
||||
|
||||
ClippingRectangle {
|
||||
id: popupBackground
|
||||
anchors.fill: parent
|
||||
color: Appearance.m3colors.m3surface
|
||||
radius: Appearance.rounding.normal
|
||||
|
||||
ColumnLayout {
|
||||
id: contentArea
|
||||
anchors.fill: parent
|
||||
anchors.margins: root.margin
|
||||
}
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: containerHoverHandler
|
||||
enabled: root.interactable
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
root.instance = popoutWindow;
|
||||
for (let i = 0; i < root.content.length; i++) {
|
||||
const comp = root.content[i];
|
||||
if (comp && comp.createObject) {
|
||||
comp.createObject(contentArea);
|
||||
} else {
|
||||
console.warn("StyledPopout: invalid content:", comp);
|
||||
}
|
||||
}
|
||||
|
||||
let parentPopout = root.parent;
|
||||
while (parentPopout && !parentPopout.childPopouts)
|
||||
parentPopout = parentPopout.parent;
|
||||
|
||||
if (parentPopout) {
|
||||
parentPopout.childPopouts.push(root);
|
||||
if (parentPopout.item)
|
||||
popoutWindow.parentPopoutWindow = parentPopout.item;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import qs.config
|
||||
import QtQuick
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
Behavior on color {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
ColorAnimation {
|
||||
duration: Metrics.chronoDuration(600)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.animation.curves.standard
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import qs.config
|
||||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
Slider {
|
||||
id: root
|
||||
|
||||
property real trackHeightDiff: 15
|
||||
property real handleGap: Metrics.spacing(4)
|
||||
property real trackDotSize: Metrics.iconSize(4)
|
||||
property real trackNearHandleRadius: Appearance.rounding.unsharpen
|
||||
property bool useAnim: true
|
||||
property int iconSize: Appearance.font.size.large
|
||||
property string icon: ""
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
implicitWidth: 200
|
||||
implicitHeight: 40
|
||||
from: 0
|
||||
to: 100
|
||||
value: 0
|
||||
stepSize: 0
|
||||
snapMode: stepSize > 0 ? Slider.SnapAlways : Slider.NoSnap
|
||||
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onPressed: (mouse) => mouse.accepted = false
|
||||
cursorShape: root.pressed ? Qt.ClosedHandCursor : Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
MaterialSymbol {
|
||||
id: icon
|
||||
icon: root.icon
|
||||
iconSize: root.iconSize
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.rightMargin: Metrics.margin(16)
|
||||
}
|
||||
|
||||
background: Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
|
||||
// Filled Left Segment
|
||||
Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
|
||||
width: root.handleGap + (root.visualPosition * (root.width - root.handleGap * 2))
|
||||
- ((root.pressed ? 1.5 : 3) / 2 + root.handleGap)
|
||||
|
||||
height: root.height - root.trackHeightDiff
|
||||
color: Appearance.colors.colPrimary
|
||||
radius: Metrics.radius("small")
|
||||
topRightRadius: root.trackNearHandleRadius
|
||||
bottomRightRadius: root.trackNearHandleRadius
|
||||
|
||||
Behavior on width {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
NumberAnimation {
|
||||
duration: !root.useAnim ? 0 : Metrics.chronoDuration("small")
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.animation.curves.expressiveEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remaining Right Segment
|
||||
Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
|
||||
width: root.handleGap + ((1 - root.visualPosition) * (root.width - root.handleGap * 2))
|
||||
- ((root.pressed ? 1.5 : 3) / 2 + root.handleGap)
|
||||
|
||||
height: root.height - root.trackHeightDiff
|
||||
color: Appearance.colors.colSecondaryContainer
|
||||
radius: Metrics.radius("small")
|
||||
topLeftRadius: root.trackNearHandleRadius
|
||||
bottomLeftRadius: root.trackNearHandleRadius
|
||||
|
||||
Behavior on width {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
NumberAnimation {
|
||||
duration: !root.useAnim ? 0 : Metrics.chronoDuration("small")
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.animation.curves.expressiveEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
handle: Rectangle {
|
||||
width: 5
|
||||
height: root.height
|
||||
radius: (width / 2) * Config.runtime.appearance.rounding.factor
|
||||
|
||||
x: root.handleGap + (root.visualPosition * (root.width - root.handleGap * 2)) - width / 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
color: Appearance.colors.colPrimary
|
||||
|
||||
Behavior on x {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
NumberAnimation {
|
||||
duration: !root.useAnim ? 0 : Appearance.animation.elementMoveFast.duration
|
||||
easing.type: Appearance.animation.elementMoveFast.type
|
||||
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
Item {
|
||||
id: root
|
||||
width: 60
|
||||
height: 34
|
||||
|
||||
property bool checked: false
|
||||
signal toggled(bool checked)
|
||||
|
||||
// Colors
|
||||
property color trackOn: Appearance.colors.colPrimary
|
||||
property color trackOff: Appearance.colors.colLayer2
|
||||
property color outline: Appearance.colors.colOutline
|
||||
|
||||
property color thumbOn: Appearance.colors.colOnPrimary
|
||||
property color thumbOff: Appearance.colors.colOnLayer2
|
||||
|
||||
property color iconOn: Appearance.colors.colPrimary
|
||||
property color iconOff: Appearance.colors.colOnPrimary
|
||||
|
||||
// Dimensions
|
||||
property int trackRadius: (height / 2) * Config.runtime.appearance.rounding.factor
|
||||
property int thumbSize: height - (checked ? 10 : 14)
|
||||
|
||||
Behavior on thumbSize {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
NumberAnimation {
|
||||
duration: Metrics.chronoDuration("normal")
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.animation.curves.expressiveEffects
|
||||
}
|
||||
}
|
||||
|
||||
// TRACK
|
||||
Rectangle {
|
||||
id: track
|
||||
anchors.fill: parent
|
||||
radius: trackRadius
|
||||
|
||||
color: root.checked ? trackOn : trackOff
|
||||
border.width: root.checked ? 0 : 2
|
||||
border.color: outline
|
||||
|
||||
Behavior on color {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
ColorAnimation {
|
||||
duration: Metrics.chronoDuration("normal")
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.animation.curves.expressiveEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// THUMB
|
||||
Rectangle {
|
||||
id: thumb
|
||||
width: thumbSize
|
||||
height: thumbSize
|
||||
radius: (thumbSize / 2) * Config.runtime.appearance.rounding.factor
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
x: root.checked ? parent.width - width - 6 : 6
|
||||
|
||||
color: root.checked ? thumbOn : thumbOff
|
||||
|
||||
Behavior on x {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
NumberAnimation {
|
||||
duration: Metrics.chronoDuration("small")
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.animation.curves.expressiveEffects
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
ColorAnimation {
|
||||
duration: Metrics.chronoDuration("small")
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.animation.curves.expressiveEffects
|
||||
}
|
||||
}
|
||||
|
||||
// ✓ CHECK ICON
|
||||
MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
icon: "check"
|
||||
iconSize: parent.width * 0.7
|
||||
color: iconOn
|
||||
|
||||
opacity: root.checked ? 1 : 0
|
||||
scale: root.checked ? 1 : 0.6
|
||||
|
||||
Behavior on opacity { NumberAnimation { duration: 120 } }
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: 160
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ✕ CROSS ICON (more visible)
|
||||
MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
icon: "close"
|
||||
iconSize: parent.width * 0.72
|
||||
color: iconOff
|
||||
|
||||
opacity: root.checked ? 0 : 1
|
||||
scale: root.checked ? 0.6 : 1
|
||||
|
||||
Behavior on opacity { NumberAnimation { duration: 120 } }
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: 160
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: {
|
||||
root.checked = !root.checked
|
||||
root.toggled(root.checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import qs.config
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
RowLayout {
|
||||
id: main
|
||||
property string title: "Title"
|
||||
property string description: "Description"
|
||||
property string prefField: ''
|
||||
|
||||
ColumnLayout {
|
||||
StyledText { text: main.title; font.pixelSize: Metrics.fontSize(16); }
|
||||
StyledText { text: main.description; font.pixelSize: Metrics.fontSize(12); }
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
StyledSwitch {
|
||||
// Safely resolve nested key (e.g. "background.showClock" or "bar.modules.some.setting")
|
||||
checked: {
|
||||
if (!main.prefField) return false;
|
||||
var parts = main.prefField.split('.');
|
||||
var cur = Config.runtime;
|
||||
for (var i = 0; i < parts.length; ++i) {
|
||||
if (cur === undefined || cur === null) return false;
|
||||
cur = cur[parts[i]];
|
||||
}
|
||||
// If the config value is undefined, default to false
|
||||
return cur === undefined || cur === null ? false : cur;
|
||||
}
|
||||
|
||||
onToggled: {
|
||||
// Persist change (updateKey will create missing objects)
|
||||
Config.updateKey(main.prefField, checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.config
|
||||
import QtQuick
|
||||
|
||||
Text {
|
||||
id: root
|
||||
|
||||
// from github.com/yannpelletier/twinshell with modifications
|
||||
|
||||
property bool animate: true
|
||||
property string animateProp: "scale"
|
||||
property real animateFrom: 0
|
||||
property real animateTo: 1
|
||||
property int animateDuration: Metrics.chronoDuration("small")
|
||||
|
||||
renderType: Text.NativeRendering
|
||||
textFormat: Text.PlainText
|
||||
color: Appearance.syntaxHighlightingTheme
|
||||
font.family: Metrics.fontFamily("main")
|
||||
font.pixelSize: Metrics.fontSize("normal")
|
||||
|
||||
Behavior on color {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
ColorAnimation {
|
||||
duration: Metrics.chronoDuration("small")
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.animation.curves.standard
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on text {
|
||||
enabled: Config.runtime.appearance.animations.enabled && root.animate
|
||||
|
||||
SequentialAnimation {
|
||||
Anim {
|
||||
to: root.animateFrom
|
||||
easing.bezierCurve: Appearance.animation.curves.standardAccel
|
||||
}
|
||||
PropertyAction {}
|
||||
Anim {
|
||||
to: root.animateTo
|
||||
easing.bezierCurve: Appearance.animation.curves.standardDecel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component Anim: NumberAnimation {
|
||||
target: root
|
||||
property: root.animateProp
|
||||
duration: root.animateDuration / 2
|
||||
easing.type: Easing.BezierSpline
|
||||
}
|
||||
}
|
||||
195
.config/quickshell/nucleus-shell/modules/components/StyledTextField.qml
Executable file
195
.config/quickshell/nucleus-shell/modules/components/StyledTextField.qml
Executable file
@@ -0,0 +1,195 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import qs.config
|
||||
import qs.modules.functions
|
||||
|
||||
TextField {
|
||||
id: control
|
||||
|
||||
property string icon: ""
|
||||
property color iconColor: Appearance.m3colors.m3onSurfaceVariant
|
||||
property string placeholder: ""
|
||||
property real iconSize: Metrics.iconSize(24)
|
||||
property alias radius: bg.radius
|
||||
property bool outline: true
|
||||
property alias topLeftRadius: bg.topLeftRadius
|
||||
property alias topRightRadius: bg.topRightRadius
|
||||
property alias bottomLeftRadius: bg.bottomLeftRadius
|
||||
property alias bottomRightRadius: bg.bottomRightRadius
|
||||
property color backgroundColor: filled ? Appearance.m3colors.m3surfaceContainerHigh : "transparent"
|
||||
property int fieldPadding: Metrics.padding(20)
|
||||
property int iconSpacing: Metrics.spacing(14)
|
||||
property int iconMargin: Metrics.margin(20)
|
||||
property bool filled: true
|
||||
property bool highlight: true
|
||||
|
||||
width: parent ? parent.width - 40 : 300
|
||||
placeholderText: placeholder
|
||||
leftPadding: icon !== "" ? iconSize + iconSpacing + iconMargin : fieldPadding
|
||||
padding: fieldPadding
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
placeholderTextColor: Appearance.m3colors.m3onSurfaceVariant
|
||||
font.family: "Outfit"
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
cursorVisible: control.focus
|
||||
|
||||
MaterialSymbol {
|
||||
icon: control.icon
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: icon !== "" ? iconMargin : 0
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
font.pixelSize: control.iconSize
|
||||
color: control.iconColor
|
||||
visible: control.icon !== ""
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Metrics.chronoDuration("small")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
cursorDelegate: Rectangle {
|
||||
width: 2
|
||||
color: Appearance.m3colors.m3primary
|
||||
visible: control.focus
|
||||
|
||||
SequentialAnimation on opacity {
|
||||
loops: Animation.Infinite
|
||||
running: control.focus && Config.runtime.appearance.animations.enabled
|
||||
|
||||
NumberAnimation {
|
||||
from: 1
|
||||
to: 0
|
||||
duration: Metrics.chronoDuration("lrage") * 2
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
from: 0
|
||||
to: 1
|
||||
duration: Metrics.chronoDuration("lrage") * 2
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
background: Item {
|
||||
Rectangle {
|
||||
id: bg
|
||||
|
||||
anchors.fill: parent
|
||||
radius: Metrics.radius("unsharpenmore")
|
||||
color: control.backgroundColor
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: {
|
||||
if (control.activeFocus && control.highlight)
|
||||
return ColorUtils.transparentize(Appearance.m3colors.m3primary, 0.8);
|
||||
|
||||
if (control.hovered && control.highlight)
|
||||
return ColorUtils.transparentize(Appearance.m3colors.m3onSurface, 0.9);
|
||||
|
||||
return "transparent";
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
|
||||
ColorAnimation {
|
||||
duration: Metrics.chronoDuration("small")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: indicator
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
height: control.activeFocus ? 2 : 1
|
||||
color: {
|
||||
if (control.activeFocus)
|
||||
return Appearance.m3colors.m3primary;
|
||||
|
||||
if (control.hovered)
|
||||
return Appearance.m3colors.m3onSurface;
|
||||
|
||||
return Appearance.m3colors.m3onSurface;
|
||||
}
|
||||
visible: filled
|
||||
|
||||
Behavior on height {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
NumberAnimation {
|
||||
duration: Metrics.chronoDuration("small")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
ColorAnimation {
|
||||
duration: Metrics.chronoDuration("small")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: outline
|
||||
|
||||
anchors.fill: parent
|
||||
radius: bg.radius
|
||||
color: "transparent"
|
||||
border.width: control.activeFocus ? 2 : 1
|
||||
border.color: {
|
||||
if (control.activeFocus)
|
||||
return Appearance.m3colors.m3primary;
|
||||
|
||||
if (control.hovered)
|
||||
return Appearance.m3colors.m3onSurface;
|
||||
|
||||
return Appearance.m3colors.m3outline;
|
||||
}
|
||||
visible: !filled && control.outline
|
||||
|
||||
Behavior on border.width {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
NumberAnimation {
|
||||
duration: Metrics.chronoDuration("small")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
ColorAnimation {
|
||||
duration: Metrics.chronoDuration("small")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
22
.config/quickshell/nucleus-shell/modules/components/Tint.qml
Normal file
22
.config/quickshell/nucleus-shell/modules/components/Tint.qml
Normal file
@@ -0,0 +1,22 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import qs.config
|
||||
|
||||
Item {
|
||||
property var sourceItem: null
|
||||
|
||||
Loader {
|
||||
active: Config.runtime.appearance.tintIcons
|
||||
anchors.fill: parent
|
||||
sourceComponent: MultiEffect {
|
||||
source: sourceItem
|
||||
|
||||
saturation: -1.0
|
||||
contrast: 0.10
|
||||
brightness: -0.08
|
||||
blur: 0.0
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import QtQuick
|
||||
import "shapes/morph.js" as Morph
|
||||
import qs.config
|
||||
|
||||
// From github.com/end-4/rounded-polygons-qmljs
|
||||
|
||||
Canvas {
|
||||
id: root
|
||||
property color color: "#685496"
|
||||
property var roundedPolygon: null
|
||||
property bool polygonIsNormalized: true
|
||||
property real borderWidth: 0
|
||||
property color borderColor: color
|
||||
property bool debug: false
|
||||
|
||||
// Internals: size
|
||||
property var bounds: roundedPolygon.calculateBounds()
|
||||
implicitWidth: bounds[2] - bounds[0]
|
||||
implicitHeight: bounds[3] - bounds[1]
|
||||
|
||||
// Internals: anim
|
||||
property var prevRoundedPolygon: null
|
||||
property double progress: 1
|
||||
property var morph: new Morph.Morph(roundedPolygon, roundedPolygon)
|
||||
property Animation animation: NumberAnimation {
|
||||
duration: Metrics.chronoDuration(350)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: [0.42, 1.67, 0.21, 0.90, 1, 1] // Material 3 Expressive fast spatial (https://m3.material.io/styles/motion/overview/specs)
|
||||
}
|
||||
|
||||
onRoundedPolygonChanged: {
|
||||
delete root.morph
|
||||
root.morph = new Morph.Morph(root.prevRoundedPolygon ?? root.roundedPolygon, root.roundedPolygon)
|
||||
morphBehavior.enabled = false;
|
||||
root.progress = 0
|
||||
morphBehavior.enabled = true;
|
||||
root.progress = 1
|
||||
root.prevRoundedPolygon = root.roundedPolygon
|
||||
}
|
||||
|
||||
Behavior on progress {
|
||||
id: morphBehavior
|
||||
animation: root.animation
|
||||
}
|
||||
|
||||
onProgressChanged: requestPaint()
|
||||
onColorChanged: requestPaint()
|
||||
onBorderWidthChanged: requestPaint()
|
||||
onBorderColorChanged: requestPaint()
|
||||
onDebugChanged: requestPaint()
|
||||
onPaint: {
|
||||
var ctx = getContext("2d")
|
||||
ctx.fillStyle = root.color
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
if (!root.morph) return
|
||||
const cubics = root.morph.asCubics(root.progress)
|
||||
if (cubics.length === 0) return
|
||||
|
||||
const size = Math.min(root.width, root.height)
|
||||
|
||||
ctx.save()
|
||||
if (root.polygonIsNormalized) ctx.scale(size, size)
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(cubics[0].anchor0X, cubics[0].anchor0Y)
|
||||
for (const cubic of cubics) {
|
||||
ctx.bezierCurveTo(
|
||||
cubic.control0X, cubic.control0Y,
|
||||
cubic.control1X, cubic.control1Y,
|
||||
cubic.anchor1X, cubic.anchor1Y
|
||||
)
|
||||
}
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
|
||||
if (root.borderWidth > 0) {
|
||||
ctx.strokeStyle = root.borderColor
|
||||
ctx.lineWidth = root.borderWidth
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
if (root.debug) {
|
||||
const points = []
|
||||
for (let i = 0; i < cubics.length; ++i) {
|
||||
const c = cubics[i]
|
||||
if (i === 0)
|
||||
points.push({ x: c.anchor0X, y: c.anchor0Y })
|
||||
points.push({ x: c.anchor1X, y: c.anchor1Y })
|
||||
}
|
||||
|
||||
let radius = Metrics.radius(2)
|
||||
|
||||
ctx.fillStyle = "red"
|
||||
for (const p of points) {
|
||||
ctx.beginPath()
|
||||
ctx.arc(p.x, p.y, radius, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
}
|
||||
}
|
||||
|
||||
ctx.restore()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
.pragma library
|
||||
|
||||
/**
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @returns {Offset}
|
||||
*/
|
||||
function createOffset(x, y) {
|
||||
return new Offset(x, y);
|
||||
}
|
||||
|
||||
class Offset {
|
||||
/**
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
*/
|
||||
constructor(x, y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @returns {Offset}
|
||||
*/
|
||||
copy(x = this.x, y = this.y) {
|
||||
return new Offset(x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {number}
|
||||
*/
|
||||
getDistance() {
|
||||
return Math.sqrt(this.x * this.x + this.y * this.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {number}
|
||||
*/
|
||||
getDistanceSquared() {
|
||||
return this.x * this.x + this.y * this.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isValid() {
|
||||
return isFinite(this.x) && isFinite(this.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get isFinite() {
|
||||
return isFinite(this.x) && isFinite(this.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get isSpecified() {
|
||||
return !this.isUnspecified;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get isUnspecified() {
|
||||
return Object.is(this.x, NaN) && Object.is(this.y, NaN);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Offset}
|
||||
*/
|
||||
negate() {
|
||||
return new Offset(-this.x, -this.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Offset} other
|
||||
* @returns {Offset}
|
||||
*/
|
||||
minus(other) {
|
||||
return new Offset(this.x - other.x, this.y - other.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Offset} other
|
||||
* @returns {Offset}
|
||||
*/
|
||||
plus(other) {
|
||||
return new Offset(this.x + other.x, this.y + other.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} operand
|
||||
* @returns {Offset}
|
||||
*/
|
||||
times(operand) {
|
||||
return new Offset(this.x * operand, this.y * operand);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} operand
|
||||
* @returns {Offset}
|
||||
*/
|
||||
div(operand) {
|
||||
return new Offset(this.x / operand, this.y / operand);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} operand
|
||||
* @returns {Offset}
|
||||
*/
|
||||
rem(operand) {
|
||||
return new Offset(this.x % operand, this.y % operand);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
toString() {
|
||||
if (this.isSpecified) {
|
||||
return `Offset(${this.x.toFixed(1)}, ${this.y.toFixed(1)})`;
|
||||
} else {
|
||||
return 'Offset.Unspecified';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Offset} start
|
||||
* @param {Offset} stop
|
||||
* @param {number} fraction
|
||||
* @returns {Offset}
|
||||
*/
|
||||
static lerp(start, stop, fraction) {
|
||||
return new Offset(
|
||||
start.x + (stop.x - start.x) * fraction,
|
||||
start.y + (stop.y - start.y) * fraction
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {function(): Offset} block
|
||||
* @returns {Offset}
|
||||
*/
|
||||
takeOrElse(block) {
|
||||
return this.isSpecified ? this : block();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {number}
|
||||
*/
|
||||
angleDegrees() {
|
||||
return Math.atan2(this.y, this.x) * 180 / Math.PI;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} angle
|
||||
* @param {Offset} center
|
||||
* @returns {Offset}
|
||||
*/
|
||||
rotateDegrees(angle, center = Offset.Zero) {
|
||||
const a = angle * Math.PI / 180;
|
||||
const off = this.minus(center);
|
||||
const cosA = Math.cos(a);
|
||||
const sinA = Math.sin(a);
|
||||
const newX = off.x * cosA - off.y * sinA;
|
||||
const newY = off.x * sinA + off.y * cosA;
|
||||
return new Offset(newX, newY).plus(center);
|
||||
}
|
||||
}
|
||||
|
||||
Offset.Zero = new Offset(0, 0);
|
||||
Offset.Infinite = new Offset(Infinity, Infinity);
|
||||
Offset.Unspecified = new Offset(NaN, NaN);
|
||||
@@ -0,0 +1,198 @@
|
||||
.pragma library
|
||||
|
||||
.import "../geometry/offset.js" as Offset
|
||||
|
||||
class Matrix {
|
||||
constructor(values = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]) {
|
||||
this.values = values;
|
||||
}
|
||||
|
||||
get(row, column) {
|
||||
return this.values[(row * 4) + column];
|
||||
}
|
||||
|
||||
set(row, column, v) {
|
||||
this.values[(row * 4) + column] = v;
|
||||
}
|
||||
|
||||
/** Does the 3D transform on [point] and returns the `x` and `y` values in an [Offset]. */
|
||||
map(point) {
|
||||
if (this.values.length < 16) return point;
|
||||
|
||||
const v00 = this.get(0, 0);
|
||||
const v01 = this.get(0, 1);
|
||||
const v03 = this.get(0, 3);
|
||||
const v10 = this.get(1, 0);
|
||||
const v11 = this.get(1, 1);
|
||||
const v13 = this.get(1, 3);
|
||||
const v30 = this.get(3, 0);
|
||||
const v31 = this.get(3, 1);
|
||||
const v33 = this.get(3, 3);
|
||||
|
||||
const x = point.x;
|
||||
const y = point.y;
|
||||
const z = v03 * x + v13 * y + v33;
|
||||
const inverseZ = 1 / z;
|
||||
const pZ = isFinite(inverseZ) ? inverseZ : 0;
|
||||
|
||||
return new Offset.Offset(pZ * (v00 * x + v10 * y + v30), pZ * (v01 * x + v11 * y + v31));
|
||||
}
|
||||
|
||||
/** Multiply this matrix by [m] and assign the result to this matrix. */
|
||||
timesAssign(m) {
|
||||
const v = this.values;
|
||||
if (v.length < 16) return;
|
||||
if (m.values.length < 16) return;
|
||||
|
||||
const v00 = this.dot(0, m, 0);
|
||||
const v01 = this.dot(0, m, 1);
|
||||
const v02 = this.dot(0, m, 2);
|
||||
const v03 = this.dot(0, m, 3);
|
||||
const v10 = this.dot(1, m, 0);
|
||||
const v11 = this.dot(1, m, 1);
|
||||
const v12 = this.dot(1, m, 2);
|
||||
const v13 = this.dot(1, m, 3);
|
||||
const v20 = this.dot(2, m, 0);
|
||||
const v21 = this.dot(2, m, 1);
|
||||
const v22 = this.dot(2, m, 2);
|
||||
const v23 = this.dot(2, m, 3);
|
||||
const v30 = this.dot(3, m, 0);
|
||||
const v31 = this.dot(3, m, 1);
|
||||
const v32 = this.dot(3, m, 2);
|
||||
const v33 = this.dot(3, m, 3);
|
||||
|
||||
v[0] = v00;
|
||||
v[1] = v01;
|
||||
v[2] = v02;
|
||||
v[3] = v03;
|
||||
v[4] = v10;
|
||||
v[5] = v11;
|
||||
v[6] = v12;
|
||||
v[7] = v13;
|
||||
v[8] = v20;
|
||||
v[9] = v21;
|
||||
v[10] = v22;
|
||||
v[11] = v23;
|
||||
v[12] = v30;
|
||||
v[13] = v31;
|
||||
v[14] = v32;
|
||||
v[15] = v33;
|
||||
}
|
||||
|
||||
dot(row, m, column) {
|
||||
return this.get(row, 0) * m.get(0, column) +
|
||||
this.get(row, 1) * m.get(1, column) +
|
||||
this.get(row, 2) * m.get(2, column) +
|
||||
this.get(row, 3) * m.get(3, column);
|
||||
}
|
||||
|
||||
/** Resets the `this` to the identity matrix. */
|
||||
reset() {
|
||||
const v = this.values;
|
||||
if (v.length < 16) return;
|
||||
v[0] = 1;
|
||||
v[1] = 0;
|
||||
v[2] = 0;
|
||||
v[3] = 0;
|
||||
v[4] = 0;
|
||||
v[5] = 1;
|
||||
v[6] = 0;
|
||||
v[7] = 0;
|
||||
v[8] = 0;
|
||||
v[9] = 0;
|
||||
v[10] = 1;
|
||||
v[11] = 0;
|
||||
v[12] = 0;
|
||||
v[13] = 0;
|
||||
v[14] = 0;
|
||||
v[15] = 1;
|
||||
}
|
||||
|
||||
/** Applies a [degrees] rotation around Z to `this`. */
|
||||
rotateZ(degrees) {
|
||||
if (this.values.length < 16) return;
|
||||
|
||||
const r = degrees * (Math.PI / 180.0);
|
||||
const s = Math.sin(r);
|
||||
const c = Math.cos(r);
|
||||
|
||||
const a00 = this.get(0, 0);
|
||||
const a10 = this.get(1, 0);
|
||||
const v00 = c * a00 + s * a10;
|
||||
const v10 = -s * a00 + c * a10;
|
||||
|
||||
const a01 = this.get(0, 1);
|
||||
const a11 = this.get(1, 1);
|
||||
const v01 = c * a01 + s * a11;
|
||||
const v11 = -s * a01 + c * a11;
|
||||
|
||||
const a02 = this.get(0, 2);
|
||||
const a12 = this.get(1, 2);
|
||||
const v02 = c * a02 + s * a12;
|
||||
const v12 = -s * a02 + c * a12;
|
||||
|
||||
const a03 = this.get(0, 3);
|
||||
const a13 = this.get(1, 3);
|
||||
const v03 = c * a03 + s * a13;
|
||||
const v13 = -s * a03 + c * a13;
|
||||
|
||||
this.set(0, 0, v00);
|
||||
this.set(0, 1, v01);
|
||||
this.set(0, 2, v02);
|
||||
this.set(0, 3, v03);
|
||||
this.set(1, 0, v10);
|
||||
this.set(1, 1, v11);
|
||||
this.set(1, 2, v12);
|
||||
this.set(1, 3, v13);
|
||||
}
|
||||
|
||||
/** Scale this matrix by [x], [y], [z] */
|
||||
scale(x = 1, y = 1, z = 1) {
|
||||
if (this.values.length < 16) return;
|
||||
this.set(0, 0, this.get(0, 0) * x);
|
||||
this.set(0, 1, this.get(0, 1) * x);
|
||||
this.set(0, 2, this.get(0, 2) * x);
|
||||
this.set(0, 3, this.get(0, 3) * x);
|
||||
this.set(1, 0, this.get(1, 0) * y);
|
||||
this.set(1, 1, this.get(1, 1) * y);
|
||||
this.set(1, 2, this.get(1, 2) * y);
|
||||
this.set(1, 3, this.get(1, 3) * y);
|
||||
this.set(2, 0, this.get(2, 0) * z);
|
||||
this.set(2, 1, this.get(2, 1) * z);
|
||||
this.set(2, 2, this.get(2, 2) * z);
|
||||
this.set(2, 3, this.get(2, 3) * z);
|
||||
}
|
||||
|
||||
/** Translate this matrix by [x], [y], [z] */
|
||||
translate(x = 0, y = 0, z = 0) {
|
||||
if (this.values.length < 16) return;
|
||||
const t1 = this.get(0, 0) * x + this.get(1, 0) * y + this.get(2, 0) * z + this.get(3, 0);
|
||||
const t2 = this.get(0, 1) * x + this.get(1, 1) * y + this.get(2, 1) * z + this.get(3, 1);
|
||||
const t3 = this.get(0, 2) * x + this.get(1, 2) * y + this.get(2, 2) * z + this.get(3, 2);
|
||||
const t4 = this.get(0, 3) * x + this.get(1, 3) * y + this.get(2, 3) * z + this.get(3, 3);
|
||||
this.set(3, 0, t1);
|
||||
this.set(3, 1, t2);
|
||||
this.set(3, 2, t3);
|
||||
this.set(3, 3, t4);
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `${this.get(0, 0)} ${this.get(0, 1)} ${this.get(0, 2)} ${this.get(0, 3)}\n` +
|
||||
`${this.get(1, 0)} ${this.get(1, 1)} ${this.get(1, 2)} ${this.get(1, 3)}\n` +
|
||||
`${this.get(2, 0)} ${this.get(2, 1)} ${this.get(2, 2)} ${this.get(2, 3)}\n` +
|
||||
`${this.get(3, 0)} ${this.get(3, 1)} ${this.get(3, 2)} ${this.get(3, 3)}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Companion object constants
|
||||
Matrix.ScaleX = 0;
|
||||
Matrix.SkewY = 1;
|
||||
Matrix.Perspective0 = 3;
|
||||
Matrix.SkewX = 4;
|
||||
Matrix.ScaleY = 5;
|
||||
Matrix.Perspective1 = 7;
|
||||
Matrix.ScaleZ = 10;
|
||||
Matrix.TranslateX = 12;
|
||||
Matrix.TranslateY = 13;
|
||||
Matrix.TranslateZ = 14;
|
||||
Matrix.Perspective2 = 15;
|
||||
@@ -0,0 +1,712 @@
|
||||
.pragma library
|
||||
|
||||
.import "shapes/point.js" as Point
|
||||
.import "shapes/rounded-polygon.js" as RoundedPolygon
|
||||
.import "shapes/corner-rounding.js" as CornerRounding
|
||||
.import "geometry/offset.js" as Offset
|
||||
.import "graphics/matrix.js" as Matrix
|
||||
|
||||
var _circle = null
|
||||
var _square = null
|
||||
var _slanted = null
|
||||
var _arch = null
|
||||
var _fan = null
|
||||
var _arrow = null
|
||||
var _semiCircle = null
|
||||
var _oval = null
|
||||
var _pill = null
|
||||
var _triangle = null
|
||||
var _diamond = null
|
||||
var _clamShell = null
|
||||
var _pentagon = null
|
||||
var _gem = null
|
||||
var _verySunny = null
|
||||
var _sunny = null
|
||||
var _cookie4Sided = null
|
||||
var _cookie6Sided = null
|
||||
var _cookie7Sided = null
|
||||
var _cookie9Sided = null
|
||||
var _cookie12Sided = null
|
||||
var _ghostish = null
|
||||
var _clover4Leaf = null
|
||||
var _clover8Leaf = null
|
||||
var _burst = null
|
||||
var _softBurst = null
|
||||
var _boom = null
|
||||
var _softBoom = null
|
||||
var _flower = null
|
||||
var _puffy = null
|
||||
var _puffyDiamond = null
|
||||
var _pixelCircle = null
|
||||
var _pixelTriangle = null
|
||||
var _bun = null
|
||||
var _heart = null
|
||||
|
||||
var cornerRound15 = new CornerRounding.CornerRounding(0.15)
|
||||
var cornerRound20 = new CornerRounding.CornerRounding(0.2)
|
||||
var cornerRound30 = new CornerRounding.CornerRounding(0.3)
|
||||
var cornerRound50 = new CornerRounding.CornerRounding(0.5)
|
||||
var cornerRound100 = new CornerRounding.CornerRounding(1.0)
|
||||
|
||||
var rotateNeg30 = new Matrix.Matrix();
|
||||
rotateNeg30.rotateZ(-30);
|
||||
var rotateNeg45 = new Matrix.Matrix();
|
||||
rotateNeg45.rotateZ(-45);
|
||||
var rotateNeg90 = new Matrix.Matrix();
|
||||
rotateNeg90.rotateZ(-90);
|
||||
var rotateNeg135 = new Matrix.Matrix();
|
||||
rotateNeg135.rotateZ(-135);
|
||||
var rotate30 = new Matrix.Matrix();
|
||||
rotate30.rotateZ(30);
|
||||
var rotate45 = new Matrix.Matrix();
|
||||
rotate45.rotateZ(45);
|
||||
var rotate60 = new Matrix.Matrix();
|
||||
rotate60.rotateZ(60);
|
||||
var rotate90 = new Matrix.Matrix();
|
||||
rotate90.rotateZ(90);
|
||||
var rotate120 = new Matrix.Matrix();
|
||||
rotate120.rotateZ(120);
|
||||
var rotate135 = new Matrix.Matrix();
|
||||
rotate135.rotateZ(135);
|
||||
var rotate180 = new Matrix.Matrix();
|
||||
rotate180.rotateZ(180);
|
||||
|
||||
var rotate28th = new Matrix.Matrix();
|
||||
rotate28th.rotateZ(360/28);
|
||||
var rotateNeg16th = new Matrix.Matrix();
|
||||
rotateNeg16th.rotateZ(-360/16);
|
||||
|
||||
function getCircle() {
|
||||
if (_circle !== null) return _circle;
|
||||
_circle = circle();
|
||||
return _circle;
|
||||
}
|
||||
|
||||
function getSquare() {
|
||||
if (_square !== null) return _square;
|
||||
_square = square();
|
||||
return _square;
|
||||
}
|
||||
|
||||
function getSlanted() {
|
||||
if (_slanted !== null) return _slanted;
|
||||
_slanted = slanted();
|
||||
return _slanted;
|
||||
}
|
||||
|
||||
function getArch() {
|
||||
if (_arch !== null) return _arch;
|
||||
_arch = arch();
|
||||
return _arch;
|
||||
}
|
||||
|
||||
function getFan() {
|
||||
if (_fan !== null) return _fan;
|
||||
_fan = fan();
|
||||
return _fan;
|
||||
}
|
||||
|
||||
function getArrow() {
|
||||
if (_arrow !== null) return _arrow;
|
||||
_arrow = arrow();
|
||||
return _arrow;
|
||||
}
|
||||
|
||||
function getSemiCircle() {
|
||||
if (_semiCircle !== null) return _semiCircle;
|
||||
_semiCircle = semiCircle();
|
||||
return _semiCircle;
|
||||
}
|
||||
|
||||
function getOval() {
|
||||
if (_oval !== null) return _oval;
|
||||
_oval = oval();
|
||||
return _oval;
|
||||
}
|
||||
|
||||
function getPill() {
|
||||
if (_pill !== null) return _pill;
|
||||
_pill = pill();
|
||||
return _pill;
|
||||
}
|
||||
|
||||
function getTriangle() {
|
||||
if (_triangle !== null) return _triangle;
|
||||
_triangle = triangle();
|
||||
return _triangle;
|
||||
}
|
||||
|
||||
function getDiamond() {
|
||||
if (_diamond !== null) return _diamond;
|
||||
_diamond = diamond();
|
||||
return _diamond;
|
||||
}
|
||||
|
||||
function getClamShell() {
|
||||
if (_clamShell !== null) return _clamShell;
|
||||
_clamShell = clamShell();
|
||||
return _clamShell;
|
||||
}
|
||||
|
||||
function getPentagon() {
|
||||
if (_pentagon !== null) return _pentagon;
|
||||
_pentagon = pentagon();
|
||||
return _pentagon;
|
||||
}
|
||||
|
||||
function getGem() {
|
||||
if (_gem !== null) return _gem;
|
||||
_gem = gem();
|
||||
return _gem;
|
||||
}
|
||||
|
||||
function getSunny() {
|
||||
if (_sunny !== null) return _sunny;
|
||||
_sunny = sunny();
|
||||
return _sunny;
|
||||
}
|
||||
|
||||
function getVerySunny() {
|
||||
if (_verySunny !== null) return _verySunny;
|
||||
_verySunny = verySunny();
|
||||
return _verySunny;
|
||||
}
|
||||
|
||||
function getCookie4Sided() {
|
||||
if (_cookie4Sided !== null) return _cookie4Sided;
|
||||
_cookie4Sided = cookie4();
|
||||
return _cookie4Sided;
|
||||
}
|
||||
|
||||
function getCookie6Sided() {
|
||||
if (_cookie6Sided !== null) return _cookie6Sided;
|
||||
_cookie6Sided = cookie6();
|
||||
return _cookie6Sided;
|
||||
}
|
||||
|
||||
function getCookie7Sided() {
|
||||
if (_cookie7Sided !== null) return _cookie7Sided;
|
||||
_cookie7Sided = cookie7();
|
||||
return _cookie7Sided;
|
||||
}
|
||||
|
||||
function getCookie9Sided() {
|
||||
if (_cookie9Sided !== null) return _cookie9Sided;
|
||||
_cookie9Sided = cookie9();
|
||||
return _cookie9Sided;
|
||||
}
|
||||
|
||||
function getCookie12Sided() {
|
||||
if (_cookie12Sided !== null) return _cookie12Sided;
|
||||
_cookie12Sided = cookie12();
|
||||
return _cookie12Sided;
|
||||
}
|
||||
|
||||
function getGhostish() {
|
||||
if (_ghostish !== null) return _ghostish;
|
||||
_ghostish = ghostish();
|
||||
return _ghostish;
|
||||
}
|
||||
|
||||
function getClover4Leaf() {
|
||||
if (_clover4Leaf !== null) return _clover4Leaf;
|
||||
_clover4Leaf = clover4();
|
||||
return _clover4Leaf;
|
||||
}
|
||||
|
||||
function getClover8Leaf() {
|
||||
if (_clover8Leaf !== null) return _clover8Leaf;
|
||||
_clover8Leaf = clover8();
|
||||
return _clover8Leaf;
|
||||
}
|
||||
|
||||
function getBurst() {
|
||||
if (_burst !== null) return _burst;
|
||||
_burst = burst();
|
||||
return _burst;
|
||||
}
|
||||
|
||||
function getSoftBurst() {
|
||||
if (_softBurst !== null) return _softBurst;
|
||||
_softBurst = softBurst();
|
||||
return _softBurst;
|
||||
}
|
||||
|
||||
function getBoom() {
|
||||
if (_boom !== null) return _boom;
|
||||
_boom = boom();
|
||||
return _boom;
|
||||
}
|
||||
|
||||
function getSoftBoom() {
|
||||
if (_softBoom !== null) return _softBoom;
|
||||
_softBoom = softBoom();
|
||||
return _softBoom;
|
||||
}
|
||||
|
||||
function getFlower() {
|
||||
if (_flower !== null) return _flower;
|
||||
_flower = flower();
|
||||
return _flower;
|
||||
}
|
||||
|
||||
function getPuffy() {
|
||||
if (_puffy !== null) return _puffy;
|
||||
_puffy = puffy();
|
||||
return _puffy;
|
||||
}
|
||||
|
||||
function getPuffyDiamond() {
|
||||
if (_puffyDiamond !== null) return _puffyDiamond;
|
||||
_puffyDiamond = puffyDiamond();
|
||||
return _puffyDiamond;
|
||||
}
|
||||
|
||||
function getPixelCircle() {
|
||||
if (_pixelCircle !== null) return _pixelCircle;
|
||||
_pixelCircle = pixelCircle();
|
||||
return _pixelCircle;
|
||||
}
|
||||
|
||||
function getPixelTriangle() {
|
||||
if (_pixelTriangle !== null) return _pixelTriangle;
|
||||
_pixelTriangle = pixelTriangle();
|
||||
return _pixelTriangle;
|
||||
}
|
||||
|
||||
function getBun() {
|
||||
if (_bun !== null) return _bun;
|
||||
_bun = bun();
|
||||
return _bun;
|
||||
}
|
||||
|
||||
function getHeart() {
|
||||
if (_heart !== null) return _heart;
|
||||
_heart = heart();
|
||||
return _heart;
|
||||
}
|
||||
|
||||
function circle() {
|
||||
return RoundedPolygon.RoundedPolygon.circle(10)
|
||||
.transformed((x, y) => rotate45.map(new Offset.Offset(x, y)))
|
||||
.normalized();
|
||||
}
|
||||
|
||||
function square() {
|
||||
return RoundedPolygon.RoundedPolygon.rectangle(1, 1, cornerRound30).normalized();
|
||||
}
|
||||
|
||||
function slanted() {
|
||||
return customPolygon([
|
||||
new PointNRound(new Offset.Offset(0.926, 0.970), new CornerRounding.CornerRounding(0.189, 0.811)),
|
||||
new PointNRound(new Offset.Offset(-0.021, 0.967), new CornerRounding.CornerRounding(0.187, 0.057)),
|
||||
], 2).normalized();
|
||||
}
|
||||
|
||||
function arch() {
|
||||
return RoundedPolygon.RoundedPolygon.rectangle(1, 1, CornerRounding.Unrounded, [cornerRound20, cornerRound20, cornerRound100, cornerRound100])
|
||||
.normalized();
|
||||
}
|
||||
|
||||
function fan() {
|
||||
return customPolygon([
|
||||
new PointNRound(new Offset.Offset(1.004, 1.000), new CornerRounding.CornerRounding(0.148, 0.417)),
|
||||
new PointNRound(new Offset.Offset(0.000, 1.000), new CornerRounding.CornerRounding(0.151)),
|
||||
new PointNRound(new Offset.Offset(0.000, -0.003), new CornerRounding.CornerRounding(0.148)),
|
||||
new PointNRound(new Offset.Offset(0.978, 0.020), new CornerRounding.CornerRounding(0.803)),
|
||||
], 1).normalized();
|
||||
}
|
||||
|
||||
function arrow() {
|
||||
return customPolygon([
|
||||
new PointNRound(new Offset.Offset(1.225, 1.060), new CornerRounding.CornerRounding(0.211)),
|
||||
new PointNRound(new Offset.Offset(0.500, 0.892), new CornerRounding.CornerRounding(0.313)),
|
||||
new PointNRound(new Offset.Offset(-0.216, 1.050), new CornerRounding.CornerRounding(0.207)),
|
||||
new PointNRound(new Offset.Offset(0.499, -0.160), new CornerRounding.CornerRounding(0.215, 1.000)),
|
||||
], 1).normalized();
|
||||
}
|
||||
|
||||
function semiCircle() {
|
||||
return RoundedPolygon.RoundedPolygon.rectangle(1.6, 1, CornerRounding.Unrounded, [cornerRound20, cornerRound20, cornerRound100, cornerRound100]).normalized();
|
||||
}
|
||||
|
||||
function oval() {
|
||||
const scaleMatrix = new Matrix.Matrix();
|
||||
scaleMatrix.scale(1, 0.64);
|
||||
return RoundedPolygon.RoundedPolygon.circle()
|
||||
.transformed((x, y) => rotateNeg90.map(new Offset.Offset(x, y)))
|
||||
.transformed((x, y) => scaleMatrix.map(new Offset.Offset(x, y)))
|
||||
.transformed((x, y) => rotate135.map(new Offset.Offset(x, y)))
|
||||
.normalized();
|
||||
}
|
||||
|
||||
function pill() {
|
||||
return customPolygon([
|
||||
// new PointNRound(new Offset.Offset(0.609, 0.000), new CornerRounding.CornerRounding(1.000)),
|
||||
new PointNRound(new Offset.Offset(0.428, -0.001), new CornerRounding.CornerRounding(0.426)),
|
||||
new PointNRound(new Offset.Offset(0.961, 0.039), new CornerRounding.CornerRounding(0.426)),
|
||||
new PointNRound(new Offset.Offset(1.001, 0.428)),
|
||||
new PointNRound(new Offset.Offset(1.000, 0.609), new CornerRounding.CornerRounding(1.000)),
|
||||
], 2)
|
||||
.transformed((x, y) => rotate180.map(new Offset.Offset(x, y)))
|
||||
.normalized();
|
||||
}
|
||||
|
||||
function triangle() {
|
||||
return RoundedPolygon.RoundedPolygon.fromNumVertices(3, 1, 0.5, 0.5, cornerRound20)
|
||||
.transformed((x, y) => rotate30.map(new Offset.Offset(x, y)))
|
||||
.normalized()
|
||||
}
|
||||
|
||||
function diamond() {
|
||||
return customPolygon([
|
||||
new PointNRound(new Offset.Offset(0.500, 1.096), new CornerRounding.CornerRounding(0.151, 0.524)),
|
||||
new PointNRound(new Offset.Offset(0.040, 0.500), new CornerRounding.CornerRounding(0.159)),
|
||||
], 2).normalized();
|
||||
}
|
||||
|
||||
function clamShell() {
|
||||
return customPolygon([
|
||||
new PointNRound(new Offset.Offset(0.829, 0.841), new CornerRounding.CornerRounding(0.159)),
|
||||
new PointNRound(new Offset.Offset(0.171, 0.841), new CornerRounding.CornerRounding(0.159)),
|
||||
new PointNRound(new Offset.Offset(-0.020, 0.500), new CornerRounding.CornerRounding(0.140)),
|
||||
], 2).normalized();
|
||||
}
|
||||
|
||||
function pentagon() {
|
||||
return customPolygon([
|
||||
new PointNRound(new Offset.Offset(0.828, 0.970), new CornerRounding.CornerRounding(0.169)),
|
||||
new PointNRound(new Offset.Offset(0.172, 0.970), new CornerRounding.CornerRounding(0.169)),
|
||||
new PointNRound(new Offset.Offset(-0.030, 0.365), new CornerRounding.CornerRounding(0.164)),
|
||||
new PointNRound(new Offset.Offset(0.500, -0.009), new CornerRounding.CornerRounding(0.172)),
|
||||
new PointNRound(new Offset.Offset(1.030, 0.365), new CornerRounding.CornerRounding(0.164)),
|
||||
], 1).normalized();
|
||||
}
|
||||
|
||||
function gem() {
|
||||
return customPolygon([
|
||||
new PointNRound(new Offset.Offset(1.005, 0.792), new CornerRounding.CornerRounding(0.208)),
|
||||
new PointNRound(new Offset.Offset(0.5, 1.023), new CornerRounding.CornerRounding(0.241, 0.778)),
|
||||
new PointNRound(new Offset.Offset(-0.005, 0.792), new CornerRounding.CornerRounding(0.208)),
|
||||
new PointNRound(new Offset.Offset(0.073, 0.258), new CornerRounding.CornerRounding(0.228)),
|
||||
new PointNRound(new Offset.Offset(0.5, 0.000), new CornerRounding.CornerRounding(0.241, 0.778)),
|
||||
new PointNRound(new Offset.Offset(0.927, 0.258), new CornerRounding.CornerRounding(0.228)),
|
||||
], 1).normalized();
|
||||
}
|
||||
|
||||
function sunny() {
|
||||
return RoundedPolygon.RoundedPolygon.star(8, 1, 0.8, cornerRound15)
|
||||
.transformed((x, y) => rotate45.map(new Offset.Offset(x, y)))
|
||||
.normalized();
|
||||
}
|
||||
|
||||
function verySunny() {
|
||||
return customPolygon([
|
||||
new PointNRound(new Offset.Offset(0.500, 1.080), new CornerRounding.CornerRounding(0.085)),
|
||||
new PointNRound(new Offset.Offset(0.358, 0.843), new CornerRounding.CornerRounding(0.085)),
|
||||
], 8)
|
||||
.transformed((x, y) => rotateNeg45.map(new Offset.Offset(x, y)))
|
||||
.normalized();
|
||||
}
|
||||
|
||||
function cookie4() {
|
||||
return customPolygon([
|
||||
new PointNRound(new Offset.Offset(1.237, 1.236), new CornerRounding.CornerRounding(0.258)),
|
||||
new PointNRound(new Offset.Offset(0.500, 0.918), new CornerRounding.CornerRounding(0.233)),
|
||||
], 4).normalized();
|
||||
}
|
||||
|
||||
function cookie6() {
|
||||
return customPolygon([
|
||||
new PointNRound(new Offset.Offset(0.723, 0.884), new CornerRounding.CornerRounding(0.394)),
|
||||
new PointNRound(new Offset.Offset(0.500, 1.099), new CornerRounding.CornerRounding(0.398)),
|
||||
], 6).normalized();
|
||||
}
|
||||
|
||||
function cookie7() {
|
||||
return RoundedPolygon.RoundedPolygon.star(7, 1, 0.75, cornerRound50)
|
||||
.normalized()
|
||||
.transformed((x, y) => rotate28th.map(new Offset.Offset(x, y)))
|
||||
.transformed((x, y) => rotate28th.map(new Offset.Offset(x, y)))
|
||||
.transformed((x, y) => rotate28th.map(new Offset.Offset(x, y)))
|
||||
.transformed((x, y) => rotate28th.map(new Offset.Offset(x, y)))
|
||||
.transformed((x, y) => rotate28th.map(new Offset.Offset(x, y)))
|
||||
.normalized();
|
||||
}
|
||||
|
||||
function cookie9() {
|
||||
return RoundedPolygon.RoundedPolygon.star(9, 1, 0.8, cornerRound50)
|
||||
.transformed((x, y) => rotate30.map(new Offset.Offset(x, y)))
|
||||
.normalized();
|
||||
}
|
||||
|
||||
function cookie12() {
|
||||
return RoundedPolygon.RoundedPolygon.star(12, 1, 0.8, cornerRound50)
|
||||
.transformed((x, y) => rotate30.map(new Offset.Offset(x, y)))
|
||||
.normalized();
|
||||
}
|
||||
|
||||
function ghostish() {
|
||||
return customPolygon([
|
||||
new PointNRound(new Offset.Offset(1.000, 1.140), new CornerRounding.CornerRounding(0.254, 0.106)),
|
||||
new PointNRound(new Offset.Offset(0.575, 0.906), new CornerRounding.CornerRounding(0.253)),
|
||||
new PointNRound(new Offset.Offset(0.425, 0.906), new CornerRounding.CornerRounding(0.253)),
|
||||
new PointNRound(new Offset.Offset(0.000, 1.140), new CornerRounding.CornerRounding(0.254, 0.106)),
|
||||
new PointNRound(new Offset.Offset(0.000, 0.000), new CornerRounding.CornerRounding(1.0)),
|
||||
new PointNRound(new Offset.Offset(0.500, 0.000), new CornerRounding.CornerRounding(1.0)),
|
||||
new PointNRound(new Offset.Offset(1.000, 0.000), new CornerRounding.CornerRounding(1.0)),
|
||||
], 1).normalized();
|
||||
}
|
||||
|
||||
function clover4() {
|
||||
return customPolygon([
|
||||
new PointNRound(new Offset.Offset(1.099, 0.725), new CornerRounding.CornerRounding(0.476)),
|
||||
new PointNRound(new Offset.Offset(0.725, 1.099), new CornerRounding.CornerRounding(0.476)),
|
||||
new PointNRound(new Offset.Offset(0.500, 0.926)),
|
||||
], 4).normalized();
|
||||
}
|
||||
|
||||
function clover8() {
|
||||
return customPolygon([
|
||||
new PointNRound(new Offset.Offset(0.758, 1.101), new CornerRounding.CornerRounding(0.209)),
|
||||
new PointNRound(new Offset.Offset(0.500, 0.964)),
|
||||
], 8).normalized();
|
||||
}
|
||||
|
||||
function burst() {
|
||||
return customPolygon([
|
||||
new PointNRound(new Offset.Offset(0.592, 0.842), new CornerRounding.CornerRounding(0.006)),
|
||||
new PointNRound(new Offset.Offset(0.500, 1.006), new CornerRounding.CornerRounding(0.006)),
|
||||
], 12)
|
||||
.transformed((x, y) => rotateNeg30.map(new Offset.Offset(x, y)))
|
||||
.transformed((x, y) => rotateNeg30.map(new Offset.Offset(x, y)))
|
||||
.normalized();
|
||||
}
|
||||
|
||||
function softBurst() {
|
||||
return customPolygon([
|
||||
new PointNRound(new Offset.Offset(0.193, 0.277), new CornerRounding.CornerRounding(0.053)),
|
||||
new PointNRound(new Offset.Offset(0.176, 0.055), new CornerRounding.CornerRounding(0.053)),
|
||||
], 10)
|
||||
.transformed((x, y) => rotate180.map(new Offset.Offset(x, y)))
|
||||
.normalized();
|
||||
}
|
||||
|
||||
function boom() {
|
||||
return customPolygon([
|
||||
new PointNRound(new Offset.Offset(0.457, 0.296), new CornerRounding.CornerRounding(0.007)),
|
||||
new PointNRound(new Offset.Offset(0.500, -0.051), new CornerRounding.CornerRounding(0.007)),
|
||||
], 15)
|
||||
.transformed((x, y) => rotate120.map(new Offset.Offset(x, y)))
|
||||
.normalized();
|
||||
}
|
||||
|
||||
function softBoom() {
|
||||
return customPolygon([
|
||||
new PointNRound(new Offset.Offset(0.733, 0.454)),
|
||||
new PointNRound(new Offset.Offset(0.839, 0.437), new CornerRounding.CornerRounding(0.532)),
|
||||
new PointNRound(new Offset.Offset(0.949, 0.449), new CornerRounding.CornerRounding(0.439, 1.000)),
|
||||
new PointNRound(new Offset.Offset(0.998, 0.478), new CornerRounding.CornerRounding(0.174)),
|
||||
// mirrored points
|
||||
new PointNRound(new Offset.Offset(0.998, 0.522), new CornerRounding.CornerRounding(0.174)),
|
||||
new PointNRound(new Offset.Offset(0.949, 0.551), new CornerRounding.CornerRounding(0.439, 1.000)),
|
||||
new PointNRound(new Offset.Offset(0.839, 0.563), new CornerRounding.CornerRounding(0.532)),
|
||||
new PointNRound(new Offset.Offset(0.733, 0.546)),
|
||||
], 16)
|
||||
.transformed((x, y) => rotate45.map(new Offset.Offset(x, y)))
|
||||
.transformed((x, y) => rotateNeg16th.map(new Offset.Offset(x, y)))
|
||||
.normalized();
|
||||
}
|
||||
|
||||
function flower() {
|
||||
return customPolygon([
|
||||
new PointNRound(new Offset.Offset(0.370, 0.187)),
|
||||
new PointNRound(new Offset.Offset(0.416, 0.049), new CornerRounding.CornerRounding(0.381)),
|
||||
new PointNRound(new Offset.Offset(0.479, 0.001), new CornerRounding.CornerRounding(0.095)),
|
||||
// mirrored points
|
||||
new PointNRound(new Offset.Offset(0.521, 0.001), new CornerRounding.CornerRounding(0.095)),
|
||||
new PointNRound(new Offset.Offset(0.584, 0.049), new CornerRounding.CornerRounding(0.381)),
|
||||
new PointNRound(new Offset.Offset(0.630, 0.187)),
|
||||
], 8)
|
||||
.transformed((x, y) => rotate135.map(new Offset.Offset(x, y)))
|
||||
.normalized();
|
||||
}
|
||||
|
||||
function puffy() {
|
||||
const m = new Matrix.Matrix();
|
||||
m.scale(1, 0.742);
|
||||
const shape = customPolygon([
|
||||
// mirrored points
|
||||
new PointNRound(new Offset.Offset(1.003, 0.563), new CornerRounding.CornerRounding(0.255)),
|
||||
new PointNRound(new Offset.Offset(0.940, 0.656), new CornerRounding.CornerRounding(0.126)),
|
||||
new PointNRound(new Offset.Offset(0.881, 0.654)),
|
||||
new PointNRound(new Offset.Offset(0.926, 0.711), new CornerRounding.CornerRounding(0.660)),
|
||||
new PointNRound(new Offset.Offset(0.914, 0.851), new CornerRounding.CornerRounding(0.660)),
|
||||
new PointNRound(new Offset.Offset(0.777, 0.998), new CornerRounding.CornerRounding(0.360)),
|
||||
new PointNRound(new Offset.Offset(0.722, 0.872)),
|
||||
new PointNRound(new Offset.Offset(0.717, 0.934), new CornerRounding.CornerRounding(0.574)),
|
||||
new PointNRound(new Offset.Offset(0.670, 1.035), new CornerRounding.CornerRounding(0.426)),
|
||||
new PointNRound(new Offset.Offset(0.545, 1.040), new CornerRounding.CornerRounding(0.405)),
|
||||
new PointNRound(new Offset.Offset(0.500, 0.947)),
|
||||
// original points
|
||||
new PointNRound(new Offset.Offset(0.500, 1-0.053)),
|
||||
new PointNRound(new Offset.Offset(1-0.545, 1+0.040), new CornerRounding.CornerRounding(0.405)),
|
||||
new PointNRound(new Offset.Offset(1-0.670, 1+0.035), new CornerRounding.CornerRounding(0.426)),
|
||||
new PointNRound(new Offset.Offset(1-0.717, 1-0.066), new CornerRounding.CornerRounding(0.574)),
|
||||
new PointNRound(new Offset.Offset(1-0.722, 1-0.128)),
|
||||
new PointNRound(new Offset.Offset(1-0.777, 1-0.002), new CornerRounding.CornerRounding(0.360)),
|
||||
new PointNRound(new Offset.Offset(1-0.914, 1-0.149), new CornerRounding.CornerRounding(0.660)),
|
||||
new PointNRound(new Offset.Offset(1-0.926, 1-0.289), new CornerRounding.CornerRounding(0.660)),
|
||||
new PointNRound(new Offset.Offset(1-0.881, 1-0.346)),
|
||||
new PointNRound(new Offset.Offset(1-0.940, 1-0.344), new CornerRounding.CornerRounding(0.126)),
|
||||
new PointNRound(new Offset.Offset(1-1.003, 1-0.437), new CornerRounding.CornerRounding(0.255)),
|
||||
], 2);
|
||||
return shape.transformed((x, y) => m.map(new Offset.Offset(x, y))).normalized();
|
||||
}
|
||||
|
||||
function puffyDiamond() {
|
||||
return customPolygon([
|
||||
// original points
|
||||
new PointNRound(new Offset.Offset(0.870, 0.130), new CornerRounding.CornerRounding(0.146)),
|
||||
new PointNRound(new Offset.Offset(0.818, 0.357)),
|
||||
new PointNRound(new Offset.Offset(1.000, 0.332), new CornerRounding.CornerRounding(0.853)),
|
||||
// mirrored points
|
||||
new PointNRound(new Offset.Offset(1.000, 1-0.332), new CornerRounding.CornerRounding(0.853)),
|
||||
new PointNRound(new Offset.Offset(0.818, 1-0.357)),
|
||||
], 4)
|
||||
.transformed((x, y) => rotate90.map(new Offset.Offset(x, y)))
|
||||
.normalized();
|
||||
}
|
||||
|
||||
function pixelCircle() {
|
||||
return customPolygon([
|
||||
new PointNRound(new Offset.Offset(1.000, 0.704)),
|
||||
new PointNRound(new Offset.Offset(0.926, 0.704)),
|
||||
new PointNRound(new Offset.Offset(0.926, 0.852)),
|
||||
new PointNRound(new Offset.Offset(0.843, 0.852)),
|
||||
new PointNRound(new Offset.Offset(0.843, 0.935)),
|
||||
new PointNRound(new Offset.Offset(0.704, 0.935)),
|
||||
new PointNRound(new Offset.Offset(0.704, 1.000)),
|
||||
new PointNRound(new Offset.Offset(0.500, 1.000)),
|
||||
new PointNRound(new Offset.Offset(1-0.704, 1.000)),
|
||||
new PointNRound(new Offset.Offset(1-0.704, 0.935)),
|
||||
new PointNRound(new Offset.Offset(1-0.843, 0.935)),
|
||||
new PointNRound(new Offset.Offset(1-0.843, 0.852)),
|
||||
new PointNRound(new Offset.Offset(1-0.926, 0.852)),
|
||||
new PointNRound(new Offset.Offset(1-0.926, 0.704)),
|
||||
new PointNRound(new Offset.Offset(1-1.000, 0.704)),
|
||||
], 2)
|
||||
.normalized();
|
||||
}
|
||||
|
||||
function pixelTriangle() {
|
||||
return customPolygon([
|
||||
// mirrored points
|
||||
new PointNRound(new Offset.Offset(0.888, 1-0.439)),
|
||||
new PointNRound(new Offset.Offset(0.789, 1-0.439)),
|
||||
new PointNRound(new Offset.Offset(0.789, 1-0.344)),
|
||||
new PointNRound(new Offset.Offset(0.675, 1-0.344)),
|
||||
new PointNRound(new Offset.Offset(0.674, 1-0.265)),
|
||||
new PointNRound(new Offset.Offset(0.560, 1-0.265)),
|
||||
new PointNRound(new Offset.Offset(0.560, 1-0.170)),
|
||||
new PointNRound(new Offset.Offset(0.421, 1-0.170)),
|
||||
new PointNRound(new Offset.Offset(0.421, 1-0.087)),
|
||||
new PointNRound(new Offset.Offset(0.287, 1-0.087)),
|
||||
new PointNRound(new Offset.Offset(0.287, 1-0.000)),
|
||||
new PointNRound(new Offset.Offset(0.113, 1-0.000)),
|
||||
// original points
|
||||
new PointNRound(new Offset.Offset(0.110, 0.500)),
|
||||
new PointNRound(new Offset.Offset(0.113, 0.000)),
|
||||
new PointNRound(new Offset.Offset(0.287, 0.000)),
|
||||
new PointNRound(new Offset.Offset(0.287, 0.087)),
|
||||
new PointNRound(new Offset.Offset(0.421, 0.087)),
|
||||
new PointNRound(new Offset.Offset(0.421, 0.170)),
|
||||
new PointNRound(new Offset.Offset(0.560, 0.170)),
|
||||
new PointNRound(new Offset.Offset(0.560, 0.265)),
|
||||
new PointNRound(new Offset.Offset(0.674, 0.265)),
|
||||
new PointNRound(new Offset.Offset(0.675, 0.344)),
|
||||
new PointNRound(new Offset.Offset(0.789, 0.344)),
|
||||
new PointNRound(new Offset.Offset(0.789, 0.439)),
|
||||
new PointNRound(new Offset.Offset(0.888, 0.439)),
|
||||
], 1).normalized();
|
||||
}
|
||||
|
||||
function bun() {
|
||||
return customPolygon([
|
||||
// original points
|
||||
new PointNRound(new Offset.Offset(0.796, 0.500)),
|
||||
new PointNRound(new Offset.Offset(0.853, 0.518), cornerRound100),
|
||||
new PointNRound(new Offset.Offset(0.992, 0.631), cornerRound100),
|
||||
new PointNRound(new Offset.Offset(0.968, 1.000), cornerRound100),
|
||||
// mirrored points
|
||||
new PointNRound(new Offset.Offset(0.032, 1-0.000), cornerRound100),
|
||||
new PointNRound(new Offset.Offset(0.008, 1-0.369), cornerRound100),
|
||||
new PointNRound(new Offset.Offset(0.147, 1-0.482), cornerRound100),
|
||||
new PointNRound(new Offset.Offset(0.204, 1-0.500)),
|
||||
], 2).normalized();
|
||||
}
|
||||
|
||||
function heart() {
|
||||
return customPolygon([
|
||||
new PointNRound(new Offset.Offset(0.782, 0.611)),
|
||||
new PointNRound(new Offset.Offset(0.499, 0.946), new CornerRounding.CornerRounding(0.000)),
|
||||
new PointNRound(new Offset.Offset(0.2175, 0.611)),
|
||||
new PointNRound(new Offset.Offset(-0.064, 0.276), new CornerRounding.CornerRounding(1.000)),
|
||||
new PointNRound(new Offset.Offset(0.208, -0.066), new CornerRounding.CornerRounding(0.958)),
|
||||
new PointNRound(new Offset.Offset(0.500, 0.268), new CornerRounding.CornerRounding(0.016)),
|
||||
new PointNRound(new Offset.Offset(0.792, -0.066), new CornerRounding.CornerRounding(0.958)),
|
||||
new PointNRound(new Offset.Offset(1.064, 0.276), new CornerRounding.CornerRounding(1.000)),
|
||||
], 1)
|
||||
.normalized();
|
||||
}
|
||||
|
||||
class PointNRound {
|
||||
constructor(o, r = CornerRounding.Unrounded) {
|
||||
this.o = o;
|
||||
this.r = r;
|
||||
}
|
||||
}
|
||||
|
||||
function doRepeat(points, reps, center, mirroring) {
|
||||
if (mirroring) {
|
||||
const result = [];
|
||||
const angles = points.map(p => p.o.minus(center).angleDegrees());
|
||||
const distances = points.map(p => p.o.minus(center).getDistance());
|
||||
const actualReps = reps * 2;
|
||||
const sectionAngle = 360 / actualReps;
|
||||
for (let it = 0; it < actualReps; it++) {
|
||||
for (let index = 0; index < points.length; index++) {
|
||||
const i = (it % 2 === 0) ? index : points.length - 1 - index;
|
||||
if (i > 0 || it % 2 === 0) {
|
||||
const baseAngle = angles[i];
|
||||
const angle = it * sectionAngle + (it % 2 === 0 ? baseAngle : (2 * angles[0] - baseAngle));
|
||||
const dist = distances[i];
|
||||
const rad = angle * Math.PI / 180;
|
||||
const x = center.x + dist * Math.cos(rad);
|
||||
const y = center.y + dist * Math.sin(rad);
|
||||
result.push(new PointNRound(new Offset.Offset(x, y), points[i].r));
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} else {
|
||||
const np = points.length;
|
||||
const result = [];
|
||||
for (let i = 0; i < np * reps; i++) {
|
||||
const point = points[i % np].o.rotateDegrees(Math.floor(i / np) * 360 / reps, center);
|
||||
result.push(new PointNRound(point, points[i % np].r));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
function customPolygon(pnr, reps = 1, center = new Offset.Offset(0.5, 0.5), mirroring = false) {
|
||||
const actualPoints = doRepeat(pnr, reps, center, mirroring);
|
||||
const vertices = [];
|
||||
for (const p of actualPoints) {
|
||||
vertices.push(p.o.x);
|
||||
vertices.push(p.o.y);
|
||||
}
|
||||
const perVertexRounding = actualPoints.map(p => p.r);
|
||||
return RoundedPolygon.RoundedPolygon.fromVertices(vertices, CornerRounding.Unrounded, perVertexRounding, center.x, center.y);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
.pragma library
|
||||
|
||||
/**
|
||||
* Represents corner rounding configuration
|
||||
*/
|
||||
class CornerRounding {
|
||||
/**
|
||||
* @param {float} [radius=0]
|
||||
* @param {float} [smoothing=0]
|
||||
*/
|
||||
constructor(radius = 0, smoothing = 0) {
|
||||
this.radius = radius;
|
||||
this.smoothing = smoothing;
|
||||
}
|
||||
}
|
||||
|
||||
// Static property
|
||||
CornerRounding.Unrounded = new CornerRounding();
|
||||
@@ -0,0 +1,371 @@
|
||||
.pragma library
|
||||
.import "point.js" as PointModule
|
||||
.import "utils.js" as UtilsModule
|
||||
|
||||
var Point = PointModule.Point;
|
||||
var DistanceEpsilon = UtilsModule.DistanceEpsilon;
|
||||
var interpolate = UtilsModule.interpolate;
|
||||
var directionVector = UtilsModule.directionVector;
|
||||
var distance = UtilsModule.distance;
|
||||
|
||||
/**
|
||||
* Represents a cubic Bézier curve with anchor and control points
|
||||
*/
|
||||
class Cubic {
|
||||
/**
|
||||
* @param {Array<float>} points Array of 8 numbers [anchor0X, anchor0Y, control0X, control0Y, control1X, control1Y, anchor1X, anchor1Y]
|
||||
*/
|
||||
constructor(points) {
|
||||
this.points = points;
|
||||
}
|
||||
|
||||
get anchor0X() { return this.points[0]; }
|
||||
get anchor0Y() { return this.points[1]; }
|
||||
get control0X() { return this.points[2]; }
|
||||
get control0Y() { return this.points[3]; }
|
||||
get control1X() { return this.points[4]; }
|
||||
get control1Y() { return this.points[5]; }
|
||||
get anchor1X() { return this.points[6]; }
|
||||
get anchor1Y() { return this.points[7]; }
|
||||
|
||||
/**
|
||||
* @param {Point} anchor0
|
||||
* @param {Point} control0
|
||||
* @param {Point} control1
|
||||
* @param {Point} anchor1
|
||||
* @returns {Cubic}
|
||||
*/
|
||||
static create(anchor0, control0, control1, anchor1) {
|
||||
return new Cubic([
|
||||
anchor0.x, anchor0.y,
|
||||
control0.x, control0.y,
|
||||
control1.x, control1.y,
|
||||
anchor1.x, anchor1.y
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {float} t
|
||||
* @returns {Point}
|
||||
*/
|
||||
pointOnCurve(t) {
|
||||
const u = 1 - t;
|
||||
return new Point(
|
||||
this.anchor0X * (u * u * u) +
|
||||
this.control0X * (3 * t * u * u) +
|
||||
this.control1X * (3 * t * t * u) +
|
||||
this.anchor1X * (t * t * t),
|
||||
this.anchor0Y * (u * u * u) +
|
||||
this.control0Y * (3 * t * u * u) +
|
||||
this.control1Y * (3 * t * t * u) +
|
||||
this.anchor1Y * (t * t * t)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
zeroLength() {
|
||||
return Math.abs(this.anchor0X - this.anchor1X) < DistanceEpsilon &&
|
||||
Math.abs(this.anchor0Y - this.anchor1Y) < DistanceEpsilon;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Cubic} next
|
||||
* @returns {boolean}
|
||||
*/
|
||||
convexTo(next) {
|
||||
const prevVertex = new Point(this.anchor0X, this.anchor0Y);
|
||||
const currVertex = new Point(this.anchor1X, this.anchor1Y);
|
||||
const nextVertex = new Point(next.anchor1X, next.anchor1Y);
|
||||
return convex(prevVertex, currVertex, nextVertex);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {float} value
|
||||
* @returns {boolean}
|
||||
*/
|
||||
zeroIsh(value) {
|
||||
return Math.abs(value) < DistanceEpsilon;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<float>} bounds
|
||||
* @param {boolean} [approximate=false]
|
||||
*/
|
||||
calculateBounds(bounds, approximate = false) {
|
||||
if (this.zeroLength()) {
|
||||
bounds[0] = this.anchor0X;
|
||||
bounds[1] = this.anchor0Y;
|
||||
bounds[2] = this.anchor0X;
|
||||
bounds[3] = this.anchor0Y;
|
||||
return;
|
||||
}
|
||||
|
||||
let minX = Math.min(this.anchor0X, this.anchor1X);
|
||||
let minY = Math.min(this.anchor0Y, this.anchor1Y);
|
||||
let maxX = Math.max(this.anchor0X, this.anchor1X);
|
||||
let maxY = Math.max(this.anchor0Y, this.anchor1Y);
|
||||
|
||||
if (approximate) {
|
||||
bounds[0] = Math.min(minX, Math.min(this.control0X, this.control1X));
|
||||
bounds[1] = Math.min(minY, Math.min(this.control0Y, this.control1Y));
|
||||
bounds[2] = Math.max(maxX, Math.max(this.control0X, this.control1X));
|
||||
bounds[3] = Math.max(maxY, Math.max(this.control0Y, this.control1Y));
|
||||
return;
|
||||
}
|
||||
|
||||
// Find extrema using derivatives
|
||||
const xa = -this.anchor0X + 3 * this.control0X - 3 * this.control1X + this.anchor1X;
|
||||
const xb = 2 * this.anchor0X - 4 * this.control0X + 2 * this.control1X;
|
||||
const xc = -this.anchor0X + this.control0X;
|
||||
|
||||
if (this.zeroIsh(xa)) {
|
||||
if (xb != 0) {
|
||||
const t = 2 * xc / (-2 * xb);
|
||||
if (t >= 0 && t <= 1) {
|
||||
const it = this.pointOnCurve(t).x;
|
||||
if (it < minX) minX = it;
|
||||
if (it > maxX) maxX = it;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const xs = xb * xb - 4 * xa * xc;
|
||||
if (xs >= 0) {
|
||||
const t1 = (-xb + Math.sqrt(xs)) / (2 * xa);
|
||||
if (t1 >= 0 && t1 <= 1) {
|
||||
const it = this.pointOnCurve(t1).x;
|
||||
if (it < minX) minX = it;
|
||||
if (it > maxX) maxX = it;
|
||||
}
|
||||
|
||||
const t2 = (-xb - Math.sqrt(xs)) / (2 * xa);
|
||||
if (t2 >= 0 && t2 <= 1) {
|
||||
const it = this.pointOnCurve(t2).x;
|
||||
if (it < minX) minX = it;
|
||||
if (it > maxX) maxX = it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Repeat for y coord
|
||||
const ya = -this.anchor0Y + 3 * this.control0Y - 3 * this.control1Y + this.anchor1Y;
|
||||
const yb = 2 * this.anchor0Y - 4 * this.control0Y + 2 * this.control1Y;
|
||||
const yc = -this.anchor0Y + this.control0Y;
|
||||
|
||||
if (this.zeroIsh(ya)) {
|
||||
if (yb != 0) {
|
||||
const t = 2 * yc / (-2 * yb);
|
||||
if (t >= 0 && t <= 1) {
|
||||
const it = this.pointOnCurve(t).y;
|
||||
if (it < minY) minY = it;
|
||||
if (it > maxY) maxY = it;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const ys = yb * yb - 4 * ya * yc;
|
||||
if (ys >= 0) {
|
||||
const t1 = (-yb + Math.sqrt(ys)) / (2 * ya);
|
||||
if (t1 >= 0 && t1 <= 1) {
|
||||
const it = this.pointOnCurve(t1).y;
|
||||
if (it < minY) minY = it;
|
||||
if (it > maxY) maxY = it;
|
||||
}
|
||||
|
||||
const t2 = (-yb - Math.sqrt(ys)) / (2 * ya);
|
||||
if (t2 >= 0 && t2 <= 1) {
|
||||
const it = this.pointOnCurve(t2).y;
|
||||
if (it < minY) minY = it;
|
||||
if (it > maxY) maxY = it;
|
||||
}
|
||||
}
|
||||
}
|
||||
bounds[0] = minX;
|
||||
bounds[1] = minY;
|
||||
bounds[2] = maxX;
|
||||
bounds[3] = maxY;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {float} t
|
||||
* @returns {{a: Cubic, b: Cubic}}
|
||||
*/
|
||||
split(t) {
|
||||
const u = 1 - t;
|
||||
const pointOnCurve = this.pointOnCurve(t);
|
||||
return {
|
||||
a: new Cubic([
|
||||
this.anchor0X,
|
||||
this.anchor0Y,
|
||||
this.anchor0X * u + this.control0X * t,
|
||||
this.anchor0Y * u + this.control0Y * t,
|
||||
this.anchor0X * (u * u) + this.control0X * (2 * u * t) + this.control1X * (t * t),
|
||||
this.anchor0Y * (u * u) + this.control0Y * (2 * u * t) + this.control1Y * (t * t),
|
||||
pointOnCurve.x,
|
||||
pointOnCurve.y
|
||||
]),
|
||||
b: new Cubic([
|
||||
pointOnCurve.x,
|
||||
pointOnCurve.y,
|
||||
this.control0X * (u * u) + this.control1X * (2 * u * t) + this.anchor1X * (t * t),
|
||||
this.control0Y * (u * u) + this.control1Y * (2 * u * t) + this.anchor1Y * (t * t),
|
||||
this.control1X * u + this.anchor1X * t,
|
||||
this.control1Y * u + this.anchor1Y * t,
|
||||
this.anchor1X,
|
||||
this.anchor1Y
|
||||
])
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Cubic}
|
||||
*/
|
||||
reverse() {
|
||||
return new Cubic([
|
||||
this.anchor1X, this.anchor1Y,
|
||||
this.control1X, this.control1Y,
|
||||
this.control0X, this.control0Y,
|
||||
this.anchor0X, this.anchor0Y
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Cubic} other
|
||||
* @returns {Cubic}
|
||||
*/
|
||||
plus(other) {
|
||||
return new Cubic(other.points.map((_, index) => this.points[index] + other.points[index]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {float} x
|
||||
* @returns {Cubic}
|
||||
*/
|
||||
times(x) {
|
||||
return new Cubic(this.points.map(v => v * x));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {float} x
|
||||
* @returns {Cubic}
|
||||
*/
|
||||
div(x) {
|
||||
return this.times(1 / x);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Cubic} other
|
||||
* @returns {boolean}
|
||||
*/
|
||||
equals(other) {
|
||||
return this.points.every((p, i) => other.points[i] === p);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {function(float, float): Point} f
|
||||
* @returns {Cubic}
|
||||
*/
|
||||
transformed(f) {
|
||||
const newCubic = new MutableCubic([...this.points]);
|
||||
newCubic.transform(f);
|
||||
return newCubic;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {float} x0
|
||||
* @param {float} y0
|
||||
* @param {float} x1
|
||||
* @param {float} y1
|
||||
* @returns {Cubic}
|
||||
*/
|
||||
static straightLine(x0, y0, x1, y1) {
|
||||
return new Cubic([
|
||||
x0,
|
||||
y0,
|
||||
interpolate(x0, x1, 1/3),
|
||||
interpolate(y0, y1, 1/3),
|
||||
interpolate(x0, x1, 2/3),
|
||||
interpolate(y0, y1, 2/3),
|
||||
x1,
|
||||
y1
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {float} centerX
|
||||
* @param {float} centerY
|
||||
* @param {float} x0
|
||||
* @param {float} y0
|
||||
* @param {float} x1
|
||||
* @param {float} y1
|
||||
* @returns {Cubic}
|
||||
*/
|
||||
static circularArc(centerX, centerY, x0, y0, x1, y1) {
|
||||
const p0d = directionVector(x0 - centerX, y0 - centerY);
|
||||
const p1d = directionVector(x1 - centerX, y1 - centerY);
|
||||
const rotatedP0 = p0d.rotate90();
|
||||
const rotatedP1 = p1d.rotate90();
|
||||
const clockwise = rotatedP0.dotProductScalar(x1 - centerX, y1 - centerY) >= 0;
|
||||
const cosa = p0d.dotProduct(p1d);
|
||||
|
||||
if (cosa > 0.999) {
|
||||
return Cubic.straightLine(x0, y0, x1, y1);
|
||||
}
|
||||
|
||||
const k = distance(x0 - centerX, y0 - centerY) * 4/3 *
|
||||
(Math.sqrt(2 * (1 - cosa)) - Math.sqrt(1 - cosa * cosa)) /
|
||||
(1 - cosa) * (clockwise ? 1 : -1);
|
||||
|
||||
return new Cubic([
|
||||
x0, y0,
|
||||
x0 + rotatedP0.x * k,
|
||||
y0 + rotatedP0.y * k,
|
||||
x1 - rotatedP1.x * k,
|
||||
y1 - rotatedP1.y * k,
|
||||
x1, y1
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {float} x0
|
||||
* @param {float} y0
|
||||
* @returns {Cubic}
|
||||
*/
|
||||
static empty(x0, y0) {
|
||||
return new Cubic([x0, y0, x0, y0, x0, y0, x0, y0]);
|
||||
}
|
||||
}
|
||||
|
||||
class MutableCubic extends Cubic {
|
||||
/**
|
||||
* @param {function(float, float): Point} f
|
||||
*/
|
||||
transform(f) {
|
||||
this.transformOnePoint(f, 0);
|
||||
this.transformOnePoint(f, 2);
|
||||
this.transformOnePoint(f, 4);
|
||||
this.transformOnePoint(f, 6);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Cubic} c1
|
||||
* @param {Cubic} c2
|
||||
* @param {float} progress
|
||||
*/
|
||||
interpolate(c1, c2, progress) {
|
||||
for (let i = 0; i < 8; i++) {
|
||||
this.points[i] = interpolate(c1.points[i], c2.points[i], progress);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {function(float, float): Point} f
|
||||
* @param {number} ix
|
||||
*/
|
||||
transformOnePoint(f, ix) {
|
||||
const result = f(this.points[ix], this.points[ix + 1]);
|
||||
this.points[ix] = result.x;
|
||||
this.points[ix + 1] = result.y;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
.pragma library
|
||||
.import "feature.js" as FeatureModule
|
||||
.import "float-mapping.js" as MappingModule
|
||||
.import "point.js" as PointModule
|
||||
.import "utils.js" as UtilsModule
|
||||
|
||||
var Feature = FeatureModule.Feature;
|
||||
var Corner = FeatureModule.Corner;
|
||||
var Point = PointModule.Point;
|
||||
var DoubleMapper = MappingModule.DoubleMapper;
|
||||
var progressInRange = MappingModule.progressInRange;
|
||||
var DistanceEpsilon = UtilsModule.DistanceEpsilon;
|
||||
|
||||
var IdentityMapping = [{ a: 0, b: 0 }, { a: 0.5, b: 0.5 }];
|
||||
|
||||
class ProgressableFeature {
|
||||
/**
|
||||
* @param {float} progress
|
||||
* @param {Feature} feature
|
||||
*/
|
||||
constructor(progress, feature) {
|
||||
this.progress = progress;
|
||||
this.feature = feature;
|
||||
}
|
||||
}
|
||||
|
||||
class DistanceVertex {
|
||||
/**
|
||||
* @param {float} distance
|
||||
* @param {ProgressableFeature} f1
|
||||
* @param {ProgressableFeature} f2
|
||||
*/
|
||||
constructor(distance, f1, f2) {
|
||||
this.distance = distance;
|
||||
this.f1 = f1;
|
||||
this.f2 = f2;
|
||||
}
|
||||
}
|
||||
|
||||
class MappingHelper {
|
||||
constructor() {
|
||||
this.mapping = [];
|
||||
this.usedF1 = new Set();
|
||||
this.usedF2 = new Set();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ProgressableFeature} f1
|
||||
* @param {ProgressableFeature} f2
|
||||
*/
|
||||
addMapping(f1, f2) {
|
||||
if (this.usedF1.has(f1) || this.usedF2.has(f2)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = this.mapping.findIndex(x => x.a === f1.progress);
|
||||
const insertionIndex = -index - 1;
|
||||
const n = this.mapping.length;
|
||||
|
||||
if (n >= 1) {
|
||||
const { a: before1, b: before2 } = this.mapping[(insertionIndex + n - 1) % n];
|
||||
const { a: after1, b: after2 } = this.mapping[insertionIndex % n];
|
||||
|
||||
if (
|
||||
progressDistance(f1.progress, before1) < DistanceEpsilon ||
|
||||
progressDistance(f1.progress, after1) < DistanceEpsilon ||
|
||||
progressDistance(f2.progress, before2) < DistanceEpsilon ||
|
||||
progressDistance(f2.progress, after2) < DistanceEpsilon
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (n > 1 && !progressInRange(f2.progress, before2, after2)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.mapping.splice(insertionIndex, 0, { a: f1.progress, b: f2.progress });
|
||||
this.usedF1.add(f1);
|
||||
this.usedF2.add(f2);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<ProgressableFeature>} features1
|
||||
* @param {Array<ProgressableFeature>} features2
|
||||
* @returns {DoubleMapper}
|
||||
*/
|
||||
function featureMapper(features1, features2) {
|
||||
const filteredFeatures1 = features1.filter(f => f.feature instanceof Corner);
|
||||
const filteredFeatures2 = features2.filter(f => f.feature instanceof Corner);
|
||||
|
||||
const featureProgressMapping = doMapping(filteredFeatures1, filteredFeatures2);
|
||||
return new DoubleMapper(...featureProgressMapping);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<ProgressableFeature>} features1
|
||||
* @param {Array<ProgressableFeature>} features2
|
||||
* @returns {Array<{a: float, b: float}>}
|
||||
*/
|
||||
function doMapping(features1, features2) {
|
||||
const distanceVertexList = [];
|
||||
|
||||
for (const f1 of features1) {
|
||||
for (const f2 of features2) {
|
||||
const d = featureDistSquared(f1.feature, f2.feature);
|
||||
if (d !== Number.MAX_VALUE) {
|
||||
distanceVertexList.push(new DistanceVertex(d, f1, f2));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
distanceVertexList.sort((a, b) => a.distance - b.distance);
|
||||
|
||||
// Special cases
|
||||
if (distanceVertexList.length === 0) {
|
||||
return IdentityMapping;
|
||||
} else if (distanceVertexList.length === 1) {
|
||||
const { f1, f2 } = distanceVertexList[0];
|
||||
const p1 = f1.progress;
|
||||
const p2 = f2.progress;
|
||||
return [
|
||||
{ a: p1, b: p2 },
|
||||
{ a: (p1 + 0.5) % 1, b: (p2 + 0.5) % 1 }
|
||||
];
|
||||
}
|
||||
|
||||
const helper = new MappingHelper();
|
||||
distanceVertexList.forEach(({ f1, f2 }) => helper.addMapping(f1, f2));
|
||||
return helper.mapping;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Feature} f1
|
||||
* @param {Feature} f2
|
||||
* @returns {float}
|
||||
*/
|
||||
function featureDistSquared(f1, f2) {
|
||||
if (f1 instanceof Corner && f2 instanceof Corner && f1.convex != f2.convex) {
|
||||
return Number.MAX_VALUE;
|
||||
}
|
||||
return featureRepresentativePoint(f1).minus(featureRepresentativePoint(f2)).getDistanceSquared();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Feature} feature
|
||||
* @returns {Point}
|
||||
*/
|
||||
function featureRepresentativePoint(feature) {
|
||||
const firstCubic = feature.cubics[0];
|
||||
const lastCubic = feature.cubics[feature.cubics.length - 1];
|
||||
const x = (firstCubic.anchor0X + lastCubic.anchor1X) / 2;
|
||||
const y = (firstCubic.anchor0Y + lastCubic.anchor1Y) / 2;
|
||||
return new Point(x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {float} p1
|
||||
* @param {float} p2
|
||||
* @returns {float}
|
||||
*/
|
||||
function progressDistance(p1, p2) {
|
||||
const it = Math.abs(p1 - p2);
|
||||
return Math.min(it, 1 - it);
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
.pragma library
|
||||
.import "cubic.js" as CubicModule
|
||||
|
||||
var Cubic = CubicModule.Cubic;
|
||||
|
||||
/**
|
||||
* Base class for shape features (edges and corners)
|
||||
*/
|
||||
class Feature {
|
||||
/**
|
||||
* @param {Array<Cubic>} cubics
|
||||
*/
|
||||
constructor(cubics) {
|
||||
this.cubics = cubics;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<Cubic>} cubics
|
||||
* @returns {Edge}
|
||||
*/
|
||||
buildIgnorableFeature(cubics) {
|
||||
return new Edge(cubics);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Cubic} cubic
|
||||
* @returns {Edge}
|
||||
*/
|
||||
buildEdge(cubic) {
|
||||
return new Edge([cubic]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<Cubic>} cubics
|
||||
* @returns {Corner}
|
||||
*/
|
||||
buildConvexCorner(cubics) {
|
||||
return new Corner(cubics, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<Cubic>} cubics
|
||||
* @returns {Corner}
|
||||
*/
|
||||
buildConcaveCorner(cubics) {
|
||||
return new Corner(cubics, false);
|
||||
}
|
||||
}
|
||||
|
||||
class Edge extends Feature {
|
||||
constructor(cubics) {
|
||||
super(cubics);
|
||||
this.isIgnorableFeature = true;
|
||||
this.isEdge = true;
|
||||
this.isConvexCorner = false;
|
||||
this.isConcaveCorner = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {function(float, float): Point} f
|
||||
* @returns {Feature}
|
||||
*/
|
||||
transformed(f) {
|
||||
return new Edge(this.cubics.map(c => c.transformed(f)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Feature}
|
||||
*/
|
||||
reversed() {
|
||||
return new Edge(this.cubics.map(c => c.reverse()));
|
||||
}
|
||||
}
|
||||
|
||||
class Corner extends Feature {
|
||||
/**
|
||||
* @param {Array<Cubic>} cubics
|
||||
* @param {boolean} convex
|
||||
*/
|
||||
constructor(cubics, convex) {
|
||||
super(cubics);
|
||||
this.convex = convex;
|
||||
this.isIgnorableFeature = false;
|
||||
this.isEdge = false;
|
||||
this.isConvexCorner = convex;
|
||||
this.isConcaveCorner = !convex;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {function(float, float): Point} f
|
||||
* @returns {Feature}
|
||||
*/
|
||||
transformed(f) {
|
||||
return new Corner(this.cubics.map(c => c.transformed(f)), this.convex);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Feature}
|
||||
*/
|
||||
reversed() {
|
||||
return new Corner(this.cubics.map(c => c.reverse()), !this.convex);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
.pragma library
|
||||
.import "utils.js" as UtilsModule
|
||||
|
||||
var positiveModulo = UtilsModule.positiveModulo;
|
||||
|
||||
/**
|
||||
* Maps values between two ranges
|
||||
*/
|
||||
class DoubleMapper {
|
||||
constructor(...mappings) {
|
||||
this.sourceValues = [];
|
||||
this.targetValues = [];
|
||||
|
||||
for (const mapping of mappings) {
|
||||
this.sourceValues.push(mapping.a);
|
||||
this.targetValues.push(mapping.b);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {float} x
|
||||
* @returns {float}
|
||||
*/
|
||||
map(x) {
|
||||
return linearMap(this.sourceValues, this.targetValues, x);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {float} x
|
||||
* @returns {float}
|
||||
*/
|
||||
mapBack(x) {
|
||||
return linearMap(this.targetValues, this.sourceValues, x);
|
||||
}
|
||||
}
|
||||
|
||||
// Static property
|
||||
DoubleMapper.Identity = new DoubleMapper({ a: 0, b: 0 }, { a: 0.5, b: 0.5 });
|
||||
|
||||
/**
|
||||
* @param {Array<float>} xValues
|
||||
* @param {Array<float>} yValues
|
||||
* @param {float} x
|
||||
* @returns {float}
|
||||
*/
|
||||
function linearMap(xValues, yValues, x) {
|
||||
let segmentStartIndex = -1;
|
||||
for (let i = 0; i < xValues.length; i++) {
|
||||
const nextIndex = (i + 1) % xValues.length;
|
||||
if (progressInRange(x, xValues[i], xValues[nextIndex])) {
|
||||
segmentStartIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (segmentStartIndex === -1) {
|
||||
throw new Error("No valid segment found");
|
||||
}
|
||||
|
||||
const segmentEndIndex = (segmentStartIndex + 1) % xValues.length;
|
||||
const segmentSizeX = positiveModulo(xValues[segmentEndIndex] - xValues[segmentStartIndex], 1);
|
||||
const segmentSizeY = positiveModulo(yValues[segmentEndIndex] - yValues[segmentStartIndex], 1);
|
||||
|
||||
let positionInSegment;
|
||||
if (segmentSizeX < 0.001) {
|
||||
positionInSegment = 0.5;
|
||||
} else {
|
||||
positionInSegment = positiveModulo(x - xValues[segmentStartIndex], 1) / segmentSizeX;
|
||||
}
|
||||
|
||||
return positiveModulo(yValues[segmentStartIndex] + segmentSizeY * positionInSegment, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {float} progress
|
||||
* @param {float} progressFrom
|
||||
* @param {float} progressTo
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function progressInRange(progress, progressFrom, progressTo) {
|
||||
if (progressTo >= progressFrom) {
|
||||
return progress >= progressFrom && progress <= progressTo;
|
||||
} else {
|
||||
return progress >= progressFrom || progress <= progressTo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
.pragma library
|
||||
|
||||
.import "rounded-polygon.js" as RoundedPolygon
|
||||
.import "cubic.js" as Cubic
|
||||
.import "polygon-measure.js" as PolygonMeasure
|
||||
.import "feature-mapping.js" as FeatureMapping
|
||||
.import "utils.js" as Utils
|
||||
|
||||
class Morph {
|
||||
constructor(start, end) {
|
||||
this.morphMatch = this.match(start, end)
|
||||
}
|
||||
|
||||
asCubics(progress) {
|
||||
const ret = []
|
||||
|
||||
// The first/last mechanism here ensures that the final anchor point in the shape
|
||||
// exactly matches the first anchor point. There can be rendering artifacts introduced
|
||||
// by those points being slightly off, even by much less than a pixel
|
||||
let firstCubic = null
|
||||
let lastCubic = null
|
||||
for (let i = 0; i < this.morphMatch.length; i++) {
|
||||
const cubic = new Cubic.Cubic(Array.from({ length: 8 }).map((_, it) => Utils.interpolate(
|
||||
this.morphMatch[i].a.points[it],
|
||||
this.morphMatch[i].b.points[it],
|
||||
progress,
|
||||
)))
|
||||
if (firstCubic == null)
|
||||
firstCubic = cubic
|
||||
if (lastCubic != null)
|
||||
ret.push(lastCubic)
|
||||
lastCubic = cubic
|
||||
}
|
||||
if (lastCubic != null && firstCubic != null)
|
||||
ret.push(
|
||||
new Cubic.Cubic([
|
||||
lastCubic.anchor0X,
|
||||
lastCubic.anchor0Y,
|
||||
lastCubic.control0X,
|
||||
lastCubic.control0Y,
|
||||
lastCubic.control1X,
|
||||
lastCubic.control1Y,
|
||||
firstCubic.anchor0X,
|
||||
firstCubic.anchor0Y,
|
||||
])
|
||||
)
|
||||
return ret
|
||||
}
|
||||
|
||||
forEachCubic(progress, mutableCubic, callback) {
|
||||
for (let i = 0; i < this.morphMatch.length; i++) {
|
||||
mutableCubic.interpolate(this.morphMatch[i].a, this.morphMatch[i].b, progress)
|
||||
callback(mutableCubic)
|
||||
}
|
||||
}
|
||||
|
||||
match(p1, p2) {
|
||||
const measurer = new PolygonMeasure.LengthMeasurer()
|
||||
const measuredPolygon1 = PolygonMeasure.MeasuredPolygon.measurePolygon(measurer, p1)
|
||||
const measuredPolygon2 = PolygonMeasure.MeasuredPolygon.measurePolygon(measurer, p2)
|
||||
|
||||
const features1 = measuredPolygon1.features
|
||||
const features2 = measuredPolygon2.features
|
||||
|
||||
const doubleMapper = FeatureMapping.featureMapper(features1, features2)
|
||||
|
||||
const polygon2CutPoint = doubleMapper.map(0)
|
||||
|
||||
const bs1 = measuredPolygon1
|
||||
const bs2 = measuredPolygon2.cutAndShift(polygon2CutPoint)
|
||||
|
||||
const ret = []
|
||||
|
||||
let i1 = 0
|
||||
let i2 = 0
|
||||
|
||||
let b1 = bs1.cubics[i1++]
|
||||
let b2 = bs2.cubics[i2++]
|
||||
|
||||
while (b1 != null && b2 != null) {
|
||||
const b1a = (i1 == bs1.cubics.length) ? 1 : b1.endOutlineProgress
|
||||
const b2a = (i2 == bs2.cubics.length) ? 1 : doubleMapper.mapBack(Utils.positiveModulo(b2.endOutlineProgress + polygon2CutPoint, 1))
|
||||
const minb = Math.min(b1a, b2a)
|
||||
const { a: seg1, b: newb1 } = b1a > minb + Utils.AngleEpsilon ? b1.cutAtProgress(minb) : { a: b1, b: bs1.cubics[i1++] }
|
||||
const { a: seg2, b: newb2 } = b2a > minb + Utils.AngleEpsilon ? b2.cutAtProgress(Utils.positiveModulo(doubleMapper.map(minb) - polygon2CutPoint, 1)) : { a: b2, b: bs2.cubics[i2++] }
|
||||
|
||||
ret.push({ a: seg1.cubic, b: seg2.cubic })
|
||||
b1 = newb1
|
||||
b2 = newb2
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
.pragma library
|
||||
|
||||
/**
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @returns {Point}
|
||||
*/
|
||||
function createPoint(x, y) {
|
||||
return new Point(x, y);
|
||||
}
|
||||
|
||||
class Point {
|
||||
/**
|
||||
* @param {float} x
|
||||
* @param {float} y
|
||||
*/
|
||||
constructor(x, y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {float} x
|
||||
* @param {float} y
|
||||
* @returns {Point}
|
||||
*/
|
||||
copy(x = this.x, y = this.y) {
|
||||
return new Point(x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {float}
|
||||
*/
|
||||
getDistance() {
|
||||
return Math.sqrt(this.x * this.x + this.y * this.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {float}
|
||||
*/
|
||||
getDistanceSquared() {
|
||||
return this.x * this.x + this.y * this.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Point} other
|
||||
* @returns {float}
|
||||
*/
|
||||
dotProduct(other) {
|
||||
return this.x * other.x + this.y * other.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {float} otherX
|
||||
* @param {float} otherY
|
||||
* @returns {float}
|
||||
*/
|
||||
dotProductScalar(otherX, otherY) {
|
||||
return this.x * otherX + this.y * otherY;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Point} other
|
||||
* @returns {boolean}
|
||||
*/
|
||||
clockwise(other) {
|
||||
return this.x * other.y - this.y * other.x > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Point}
|
||||
*/
|
||||
getDirection() {
|
||||
const d = this.getDistance();
|
||||
return this.div(d);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Point}
|
||||
*/
|
||||
negate() {
|
||||
return new Point(-this.x, -this.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Point} other
|
||||
* @returns {Point}
|
||||
*/
|
||||
minus(other) {
|
||||
return new Point(this.x - other.x, this.y - other.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Point} other
|
||||
* @returns {Point}
|
||||
*/
|
||||
plus(other) {
|
||||
return new Point(this.x + other.x, this.y + other.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {float} operand
|
||||
* @returns {Point}
|
||||
*/
|
||||
times(operand) {
|
||||
return new Point(this.x * operand, this.y * operand);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {float} operand
|
||||
* @returns {Point}
|
||||
*/
|
||||
div(operand) {
|
||||
return new Point(this.x / operand, this.y / operand);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {float} operand
|
||||
* @returns {Point}
|
||||
*/
|
||||
rem(operand) {
|
||||
return new Point(this.x % operand, this.y % operand);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Point} start
|
||||
* @param {Point} stop
|
||||
* @param {float} fraction
|
||||
* @returns {Point}
|
||||
*/
|
||||
static interpolate(start, stop, fraction) {
|
||||
return new Point(
|
||||
start.x + (stop.x - start.x) * fraction,
|
||||
start.y + (stop.y - start.y) * fraction
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {function(float, float): Point} f
|
||||
* @returns {Point}
|
||||
*/
|
||||
transformed(f) {
|
||||
const result = f(this.x, this.y);
|
||||
return new Point(result.x, result.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Point}
|
||||
*/
|
||||
rotate90() {
|
||||
return new Point(-this.y, this.x);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
.pragma library
|
||||
|
||||
.import "cubic.js" as Cubic
|
||||
.import "point.js" as Point
|
||||
.import "feature-mapping.js" as FeatureMapping
|
||||
.import "utils.js" as Utils
|
||||
.import "feature.js" as Feature
|
||||
|
||||
class MeasuredPolygon {
|
||||
constructor(measurer, features, cubics, outlineProgress) {
|
||||
this.measurer = measurer
|
||||
this.features = features
|
||||
this.outlineProgress = outlineProgress
|
||||
this.cubics = []
|
||||
|
||||
const measuredCubics = []
|
||||
let startOutlineProgress = 0
|
||||
for(let i = 0; i < cubics.length; i++) {
|
||||
if ((outlineProgress[i + 1] - outlineProgress[i]) > Utils.DistanceEpsilon) {
|
||||
measuredCubics.push(
|
||||
new MeasuredCubic(this, cubics[i], startOutlineProgress, outlineProgress[i + 1])
|
||||
)
|
||||
// The next measured cubic will start exactly where this one ends.
|
||||
startOutlineProgress = outlineProgress[i + 1]
|
||||
}
|
||||
}
|
||||
|
||||
measuredCubics[measuredCubics.length - 1].updateProgressRange(measuredCubics[measuredCubics.length - 1].startOutlineProgress, 1)
|
||||
this.cubics = measuredCubics
|
||||
}
|
||||
|
||||
cutAndShift(cuttingPoint) {
|
||||
if (cuttingPoint < Utils.DistanceEpsilon) return this
|
||||
|
||||
// Find the index of cubic we want to cut
|
||||
const targetIndex = this.cubics.findIndex(it => cuttingPoint >= it.startOutlineProgress && cuttingPoint <= it.endOutlineProgress)
|
||||
const target = this.cubics[targetIndex]
|
||||
// Cut the target cubic.
|
||||
// b1, b2 are two resulting cubics after cut
|
||||
const { a: b1, b: b2 } = target.cutAtProgress(cuttingPoint)
|
||||
|
||||
// Construct the list of the cubics we need:
|
||||
// * The second part of the target cubic (after the cut)
|
||||
// * All cubics after the target, until the end + All cubics from the start, before the
|
||||
// target cubic
|
||||
// * The first part of the target cubic (before the cut)
|
||||
const retCubics = [b2.cubic]
|
||||
for(let i = 1; i < this.cubics.length; i++) {
|
||||
retCubics.push(this.cubics[(i + targetIndex) % this.cubics.length].cubic)
|
||||
}
|
||||
retCubics.push(b1.cubic)
|
||||
|
||||
// Construct the array of outline progress.
|
||||
// For example, if we have 3 cubics with outline progress [0 .. 0.3], [0.3 .. 0.8] &
|
||||
// [0.8 .. 1.0], and we cut + shift at 0.6:
|
||||
// 0. 0123456789
|
||||
// |--|--/-|-|
|
||||
// The outline progresses will start at 0 (the cutting point, that shifs to 0.0),
|
||||
// then 0.8 - 0.6 = 0.2, then 1 - 0.6 = 0.4, then 0.3 - 0.6 + 1 = 0.7,
|
||||
// then 1 (the cutting point again),
|
||||
// all together: (0.0, 0.2, 0.4, 0.7, 1.0)
|
||||
const retOutlineProgress = []
|
||||
for (let i = 0; i < this.cubics.length + 2; i++) {
|
||||
if (i === 0) {
|
||||
retOutlineProgress.push(0)
|
||||
} else if(i === this.cubics.length + 1) {
|
||||
retOutlineProgress.push(1)
|
||||
} else {
|
||||
const cubicIndex = (targetIndex + i - 1) % this.cubics.length
|
||||
retOutlineProgress.push(Utils.positiveModulo(this.cubics[cubicIndex].endOutlineProgress - cuttingPoint, 1))
|
||||
}
|
||||
}
|
||||
|
||||
// Shift the feature's outline progress too.
|
||||
const newFeatures = []
|
||||
for(let i = 0; i < this.features.length; i++) {
|
||||
newFeatures.push(new FeatureMapping.ProgressableFeature(Utils.positiveModulo(this.features[i].progress - cuttingPoint, 1), this.features[i].feature))
|
||||
}
|
||||
|
||||
// Filter out all empty cubics (i.e. start and end anchor are (almost) the same point.)
|
||||
return new MeasuredPolygon(this.measurer, newFeatures, retCubics, retOutlineProgress)
|
||||
}
|
||||
|
||||
static measurePolygon(measurer, polygon) {
|
||||
const cubics = []
|
||||
const featureToCubic = []
|
||||
|
||||
for (let featureIndex = 0; featureIndex < polygon.features.length; featureIndex++) {
|
||||
const feature = polygon.features[featureIndex]
|
||||
for (let cubicIndex = 0; cubicIndex < feature.cubics.length; cubicIndex++) {
|
||||
if (feature instanceof Feature.Corner && cubicIndex == feature.cubics.length / 2) {
|
||||
featureToCubic.push({ a: feature, b: cubics.length })
|
||||
}
|
||||
cubics.push(feature.cubics[cubicIndex])
|
||||
}
|
||||
}
|
||||
|
||||
const measures = [0] // Initialize with 0 like in Kotlin's scan
|
||||
for (const cubic of cubics) {
|
||||
const measurement = measurer.measureCubic(cubic)
|
||||
if (measurement < 0) {
|
||||
throw new Error("Measured cubic is expected to be greater or equal to zero")
|
||||
}
|
||||
const lastMeasure = measures[measures.length - 1]
|
||||
measures.push(lastMeasure + measurement)
|
||||
}
|
||||
const totalMeasure = measures[measures.length - 1]
|
||||
|
||||
const outlineProgress = []
|
||||
for (let i = 0; i < measures.length; i++) {
|
||||
outlineProgress.push(measures[i] / totalMeasure)
|
||||
}
|
||||
|
||||
const features = []
|
||||
for (let i = 0; i < featureToCubic.length; i++) {
|
||||
const ix = featureToCubic[i].b
|
||||
features.push(
|
||||
new FeatureMapping.ProgressableFeature(Utils.positiveModulo((outlineProgress[ix] + outlineProgress[ix + 1]) / 2, 1), featureToCubic[i].a))
|
||||
}
|
||||
|
||||
return new MeasuredPolygon(measurer, features, cubics, outlineProgress)
|
||||
}
|
||||
}
|
||||
|
||||
class MeasuredCubic {
|
||||
constructor(polygon, cubic, startOutlineProgress, endOutlineProgress) {
|
||||
this.polygon = polygon
|
||||
this.cubic = cubic
|
||||
this.startOutlineProgress = startOutlineProgress
|
||||
this.endOutlineProgress = endOutlineProgress
|
||||
this.measuredSize = this.polygon.measurer.measureCubic(cubic)
|
||||
}
|
||||
|
||||
updateProgressRange(
|
||||
startOutlineProgress = this.startOutlineProgress,
|
||||
endOutlineProgress = this.endOutlineProgress,
|
||||
) {
|
||||
this.startOutlineProgress = startOutlineProgress
|
||||
this.endOutlineProgress = endOutlineProgress
|
||||
}
|
||||
|
||||
cutAtProgress(cutOutlineProgress) {
|
||||
const boundedCutOutlineProgress = Utils.coerceIn(cutOutlineProgress, this.startOutlineProgress, this.endOutlineProgress)
|
||||
const outlineProgressSize = this.endOutlineProgress - this.startOutlineProgress
|
||||
const progressFromStart = boundedCutOutlineProgress - this.startOutlineProgress
|
||||
|
||||
const relativeProgress = progressFromStart / outlineProgressSize
|
||||
const t = this.polygon.measurer.findCubicCutPoint(this.cubic, relativeProgress * this.measuredSize)
|
||||
|
||||
const {a: c1, b: c2} = this.cubic.split(t)
|
||||
return {
|
||||
a: new MeasuredCubic(this.polygon, c1, this.startOutlineProgress, boundedCutOutlineProgress),
|
||||
b: new MeasuredCubic(this.polygon, c2, boundedCutOutlineProgress, this.endOutlineProgress)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class LengthMeasurer {
|
||||
constructor() {
|
||||
this.segments = 3
|
||||
}
|
||||
|
||||
measureCubic(c) {
|
||||
return this.closestProgressTo(c, Number.POSITIVE_INFINITY).y
|
||||
}
|
||||
|
||||
findCubicCutPoint(c, m) {
|
||||
return this.closestProgressTo(c, m).x
|
||||
}
|
||||
|
||||
closestProgressTo(cubic, threshold) {
|
||||
let total = 0
|
||||
let remainder = threshold
|
||||
let prev = new Point.Point(cubic.anchor0X, cubic.anchor0Y)
|
||||
|
||||
for (let i = 1; i < this.segments; i++) {
|
||||
const progress = i / this.segments
|
||||
const point = cubic.pointOnCurve(progress)
|
||||
const segment = point.minus(prev).getDistance()
|
||||
|
||||
if (segment >= remainder) {
|
||||
return new Point.Point(progress - (1.0 - remainder / segment) / this.segments, threshold)
|
||||
}
|
||||
|
||||
remainder -= segment
|
||||
total += segment
|
||||
prev = point
|
||||
}
|
||||
|
||||
return new Point.Point(1.0, total)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
.pragma library
|
||||
.import "point.js" as PointModule
|
||||
.import "corner-rounding.js" as RoundingModule
|
||||
.import "utils.js" as UtilsModule
|
||||
.import "cubic.js" as CubicModule
|
||||
|
||||
var Point = PointModule.Point;
|
||||
var CornerRounding = RoundingModule.CornerRounding;
|
||||
var DistanceEpsilon = UtilsModule.DistanceEpsilon;
|
||||
var directionVector = UtilsModule.directionVector;
|
||||
var Cubic = CubicModule.Cubic;
|
||||
|
||||
class RoundedCorner {
|
||||
/**
|
||||
* @param {Point} p0
|
||||
* @param {Point} p1
|
||||
* @param {Point} p2
|
||||
* @param {CornerRounding} [rounding=null]
|
||||
*/
|
||||
constructor(p0, p1, p2, rounding = null) {
|
||||
this.p0 = p0;
|
||||
this.p1 = p1;
|
||||
this.p2 = p2;
|
||||
this.rounding = rounding;
|
||||
this.center = new Point(0, 0);
|
||||
|
||||
const v01 = p0.minus(p1);
|
||||
const v21 = p2.minus(p1);
|
||||
const d01 = v01.getDistance();
|
||||
const d21 = v21.getDistance();
|
||||
|
||||
if (d01 > 0 && d21 > 0) {
|
||||
this.d1 = v01.div(d01);
|
||||
this.d2 = v21.div(d21);
|
||||
this.cornerRadius = rounding?.radius ?? 0;
|
||||
this.smoothing = rounding?.smoothing ?? 0;
|
||||
|
||||
// cosine of angle at p1 is dot product of unit vectors to the other two vertices
|
||||
this.cosAngle = this.d1.dotProduct(this.d2);
|
||||
|
||||
// identity: sin^2 + cos^2 = 1
|
||||
// sinAngle gives us the intersection
|
||||
this.sinAngle = Math.sqrt(1 - Math.pow(this.cosAngle, 2));
|
||||
|
||||
// How much we need to cut, as measured on a side, to get the required radius
|
||||
// calculating where the rounding circle hits the edge
|
||||
// This uses the identity of tan(A/2) = sinA/(1 + cosA), where tan(A/2) = radius/cut
|
||||
this.expectedRoundCut = this.sinAngle > 1e-3 ? this.cornerRadius * (this.cosAngle + 1) / this.sinAngle : 0;
|
||||
} else {
|
||||
// One (or both) of the sides is empty, not much we can do.
|
||||
this.d1 = new Point(0, 0);
|
||||
this.d2 = new Point(0, 0);
|
||||
this.cornerRadius = 0;
|
||||
this.smoothing = 0;
|
||||
this.cosAngle = 0;
|
||||
this.sinAngle = 0;
|
||||
this.expectedRoundCut = 0;
|
||||
}
|
||||
}
|
||||
|
||||
get expectedCut() {
|
||||
return ((1 + this.smoothing) * this.expectedRoundCut);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {float} allowedCut0
|
||||
* @param {float} [allowedCut1]
|
||||
* @returns {Array<Cubic>}
|
||||
*/
|
||||
getCubics(allowedCut0, allowedCut1 = allowedCut0) {
|
||||
// We use the minimum of both cuts to determine the radius, but if there is more space
|
||||
// in one side we can use it for smoothing.
|
||||
const allowedCut = Math.min(allowedCut0, allowedCut1);
|
||||
|
||||
// Nothing to do, just use lines, or a point
|
||||
if (
|
||||
this.expectedRoundCut < DistanceEpsilon ||
|
||||
allowedCut < DistanceEpsilon ||
|
||||
this.cornerRadius < DistanceEpsilon
|
||||
) {
|
||||
this.center = this.p1;
|
||||
return [Cubic.straightLine(this.p1.x, this.p1.y, this.p1.x, this.p1.y)];
|
||||
}
|
||||
|
||||
// How much of the cut is required for the rounding part.
|
||||
const actualRoundCut = Math.min(allowedCut, this.expectedRoundCut);
|
||||
|
||||
// We have two smoothing values, one for each side of the vertex
|
||||
// Space is used for rounding values first. If there is space left over, then we
|
||||
// apply smoothing, if it was requested
|
||||
const actualSmoothing0 = this.calculateActualSmoothingValue(allowedCut0);
|
||||
const actualSmoothing1 = this.calculateActualSmoothingValue(allowedCut1);
|
||||
|
||||
// Scale the radius if needed
|
||||
const actualR = this.cornerRadius * actualRoundCut / this.expectedRoundCut;
|
||||
|
||||
// Distance from the corner (p1) to the center
|
||||
const centerDistance = Math.sqrt(Math.pow(actualR, 2) + Math.pow(actualRoundCut, 2));
|
||||
|
||||
// Center of the arc we will use for rounding
|
||||
this.center = this.p1.plus(this.d1.plus(this.d2).div(2).getDirection().times(centerDistance));
|
||||
|
||||
const circleIntersection0 = this.p1.plus(this.d1.times(actualRoundCut));
|
||||
const circleIntersection2 = this.p1.plus(this.d2.times(actualRoundCut));
|
||||
|
||||
const flanking0 = this.computeFlankingCurve(
|
||||
actualRoundCut,
|
||||
actualSmoothing0,
|
||||
this.p1,
|
||||
this.p0,
|
||||
circleIntersection0,
|
||||
circleIntersection2,
|
||||
this.center,
|
||||
actualR
|
||||
);
|
||||
|
||||
const flanking2 = this.computeFlankingCurve(
|
||||
actualRoundCut,
|
||||
actualSmoothing1,
|
||||
this.p1,
|
||||
this.p2,
|
||||
circleIntersection2,
|
||||
circleIntersection0,
|
||||
this.center,
|
||||
actualR
|
||||
).reverse();
|
||||
|
||||
return [
|
||||
flanking0,
|
||||
Cubic.circularArc(
|
||||
this.center.x,
|
||||
this.center.y,
|
||||
flanking0.anchor1X,
|
||||
flanking0.anchor1Y,
|
||||
flanking2.anchor0X,
|
||||
flanking2.anchor0Y
|
||||
),
|
||||
flanking2
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {float} allowedCut
|
||||
* @returns {float}
|
||||
*/
|
||||
calculateActualSmoothingValue(allowedCut) {
|
||||
if (allowedCut > this.expectedCut) {
|
||||
return this.smoothing;
|
||||
} else if (allowedCut > this.expectedRoundCut) {
|
||||
return this.smoothing * (allowedCut - this.expectedRoundCut) / (this.expectedCut - this.expectedRoundCut);
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {float} actualRoundCut
|
||||
* @param {float} actualSmoothingValues
|
||||
* @param {Point} corner
|
||||
* @param {Point} sideStart
|
||||
* @param {Point} circleSegmentIntersection
|
||||
* @param {Point} otherCircleSegmentIntersection
|
||||
* @param {Point} circleCenter
|
||||
* @param {float} actualR
|
||||
* @returns {Cubic}
|
||||
*/
|
||||
computeFlankingCurve(
|
||||
actualRoundCut,
|
||||
actualSmoothingValues,
|
||||
corner,
|
||||
sideStart,
|
||||
circleSegmentIntersection,
|
||||
otherCircleSegmentIntersection,
|
||||
circleCenter,
|
||||
actualR
|
||||
) {
|
||||
// sideStart is the anchor, 'anchor' is actual control point
|
||||
const sideDirection = (sideStart.minus(corner)).getDirection();
|
||||
const curveStart = corner.plus(sideDirection.times(actualRoundCut * (1 + actualSmoothingValues)));
|
||||
|
||||
// We use an approximation to cut a part of the circle section proportional to 1 - smooth,
|
||||
// When smooth = 0, we take the full section, when smooth = 1, we take nothing.
|
||||
const p = Point.interpolate(
|
||||
circleSegmentIntersection,
|
||||
(circleSegmentIntersection.plus(otherCircleSegmentIntersection)).div(2),
|
||||
actualSmoothingValues
|
||||
);
|
||||
|
||||
// The flanking curve ends on the circle
|
||||
const curveEnd = circleCenter.plus(
|
||||
directionVector(p.x - circleCenter.x, p.y - circleCenter.y).times(actualR)
|
||||
);
|
||||
|
||||
// The anchor on the circle segment side is in the intersection between the tangent to the
|
||||
// circle in the circle/flanking curve boundary and the linear segment.
|
||||
const circleTangent = (curveEnd.minus(circleCenter)).rotate90();
|
||||
const anchorEnd = this.lineIntersection(sideStart, sideDirection, curveEnd, circleTangent) ?? circleSegmentIntersection;
|
||||
|
||||
// From what remains, we pick a point for the start anchor.
|
||||
// 2/3 seems to come from design tools?
|
||||
const anchorStart = (curveStart.plus(anchorEnd.times(2))).div(3);
|
||||
|
||||
return Cubic.create(curveStart, anchorStart, anchorEnd, curveEnd);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Point} p0
|
||||
* @param {Point} d0
|
||||
* @param {Point} p1
|
||||
* @param {Point} d1
|
||||
* @returns {Point|null}
|
||||
*/
|
||||
lineIntersection(p0, d0, p1, d1) {
|
||||
const rotatedD1 = d1.rotate90();
|
||||
const den = d0.dotProduct(rotatedD1);
|
||||
if (Math.abs(den) < DistanceEpsilon) return null;
|
||||
|
||||
const num = (p1.minus(p0)).dotProduct(rotatedD1);
|
||||
// Also check the relative value. This is equivalent to abs(den/num) < DistanceEpsilon,
|
||||
// but avoid doing a division
|
||||
if (Math.abs(den) < DistanceEpsilon * Math.abs(num)) return null;
|
||||
|
||||
const k = num / den;
|
||||
return p0.plus(d0.times(k));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
.pragma library
|
||||
|
||||
.import "feature.js" as Feature
|
||||
.import "point.js" as Point
|
||||
.import "cubic.js" as Cubic
|
||||
.import "utils.js" as Utils
|
||||
.import "corner-rounding.js" as CornerRounding
|
||||
.import "rounded-corner.js" as RoundedCorner
|
||||
|
||||
class RoundedPolygon {
|
||||
constructor(features, center) {
|
||||
this.features = features
|
||||
this.center = center
|
||||
this.cubics = this.buildCubicList()
|
||||
}
|
||||
|
||||
get centerX() {
|
||||
return this.center.x
|
||||
}
|
||||
|
||||
get centerY() {
|
||||
return this.center.y
|
||||
}
|
||||
|
||||
transformed(f) {
|
||||
const center = this.center.transformed(f)
|
||||
return new RoundedPolygon(this.features.map(x => x.transformed(f)), center)
|
||||
}
|
||||
|
||||
normalized() {
|
||||
const bounds = this.calculateBounds()
|
||||
const width = bounds[2] - bounds[0]
|
||||
const height = bounds[3] - bounds[1]
|
||||
const side = Math.max(width, height)
|
||||
// Center the shape if bounds are not a square
|
||||
const offsetX = (side - width) / 2 - bounds[0] /* left */
|
||||
const offsetY = (side - height) / 2 - bounds[1] /* top */
|
||||
return this.transformed((x, y) => {
|
||||
return new Point.Point((x + offsetX) / side, (y + offsetY) / side)
|
||||
})
|
||||
}
|
||||
|
||||
calculateMaxBounds(bounds = []) {
|
||||
let maxDistSquared = 0
|
||||
for (let i = 0; i < this.cubics.length; i++) {
|
||||
const cubic = this.cubics[i]
|
||||
const anchorDistance = Utils.distanceSquared(cubic.anchor0X - this.centerX, cubic.anchor0Y - this.centerY)
|
||||
const middlePoint = cubic.pointOnCurve(.5)
|
||||
const middleDistance = Utils.distanceSquared(middlePoint.x - this.centerX, middlePoint.y - this.centerY)
|
||||
maxDistSquared = Math.max(maxDistSquared, Math.max(anchorDistance, middleDistance))
|
||||
}
|
||||
const distance = Math.sqrt(maxDistSquared)
|
||||
bounds[0] = this.centerX - distance
|
||||
bounds[1] = this.centerY - distance
|
||||
bounds[2] = this.centerX + distance
|
||||
bounds[3] = this.centerY + distance
|
||||
return bounds
|
||||
}
|
||||
|
||||
calculateBounds(bounds = [], approximate = true) {
|
||||
let minX = Number.MAX_SAFE_INTEGER
|
||||
let minY = Number.MAX_SAFE_INTEGER
|
||||
let maxX = Number.MIN_SAFE_INTEGER
|
||||
let maxY = Number.MIN_SAFE_INTEGER
|
||||
for (let i = 0; i < this.cubics.length; i++) {
|
||||
const cubic = this.cubics[i]
|
||||
cubic.calculateBounds(bounds, approximate)
|
||||
minX = Math.min(minX, bounds[0])
|
||||
minY = Math.min(minY, bounds[1])
|
||||
maxX = Math.max(maxX, bounds[2])
|
||||
maxY = Math.max(maxY, bounds[3])
|
||||
}
|
||||
bounds[0] = minX
|
||||
bounds[1] = minY
|
||||
bounds[2] = maxX
|
||||
bounds[3] = maxY
|
||||
return bounds
|
||||
}
|
||||
|
||||
buildCubicList() {
|
||||
const result = []
|
||||
|
||||
// The first/last mechanism here ensures that the final anchor point in the shape
|
||||
// exactly matches the first anchor point. There can be rendering artifacts introduced
|
||||
// by those points being slightly off, even by much less than a pixel
|
||||
let firstCubic = null
|
||||
let lastCubic = null
|
||||
let firstFeatureSplitStart = null
|
||||
let firstFeatureSplitEnd = null
|
||||
|
||||
if (this.features.length > 0 && this.features[0].cubics.length == 3) {
|
||||
const centerCubic = this.features[0].cubics[1]
|
||||
const { a: start, b: end } = centerCubic.split(.5)
|
||||
firstFeatureSplitStart = [this.features[0].cubics[0], start]
|
||||
firstFeatureSplitEnd = [end, this.features[0].cubics[2]]
|
||||
}
|
||||
|
||||
// iterating one past the features list size allows us to insert the initial split
|
||||
// cubic if it exists
|
||||
for (let i = 0; i <= this.features.length; i++) {
|
||||
let featureCubics
|
||||
if (i == 0 && firstFeatureSplitEnd != null) {
|
||||
featureCubics = firstFeatureSplitEnd
|
||||
} else if (i == this.features.length) {
|
||||
if (firstFeatureSplitStart != null) {
|
||||
featureCubics = firstFeatureSplitStart
|
||||
} else {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
featureCubics = this.features[i].cubics
|
||||
}
|
||||
|
||||
for (let j = 0; j < featureCubics.length; j++) {
|
||||
// Skip zero-length curves; they add nothing and can trigger rendering artifacts
|
||||
const cubic = featureCubics[j]
|
||||
if (!cubic.zeroLength()) {
|
||||
if (lastCubic != null)
|
||||
result.push(lastCubic)
|
||||
lastCubic = cubic
|
||||
if (firstCubic == null)
|
||||
firstCubic = cubic
|
||||
} else {
|
||||
if (lastCubic != null) {
|
||||
// Dropping several zero-ish length curves in a row can lead to
|
||||
// enough discontinuity to throw an exception later, even though the
|
||||
// distances are quite small. Account for that by making the last
|
||||
// cubic use the latest anchor point, always.
|
||||
lastCubic = new Cubic.Cubic([...lastCubic.points]) // Make a copy before mutating
|
||||
lastCubic.points[6] = cubic.anchor1X
|
||||
lastCubic.points[7] = cubic.anchor1Y
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (lastCubic != null && firstCubic != null) {
|
||||
result.push(
|
||||
new Cubic.Cubic([
|
||||
lastCubic.anchor0X,
|
||||
lastCubic.anchor0Y,
|
||||
lastCubic.control0X,
|
||||
lastCubic.control0Y,
|
||||
lastCubic.control1X,
|
||||
lastCubic.control1Y,
|
||||
firstCubic.anchor0X,
|
||||
firstCubic.anchor0Y,
|
||||
])
|
||||
)
|
||||
} else {
|
||||
// Empty / 0-sized polygon.
|
||||
result.push(new Cubic.Cubic([this.centerX, this.centerY, this.centerX, this.centerY, this.centerX, this.centerY, this.centerX, this.centerY]))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
static calculateCenter(vertices) {
|
||||
let cumulativeX = 0
|
||||
let cumulativeY = 0
|
||||
let index = 0
|
||||
while (index < vertices.length) {
|
||||
cumulativeX += vertices[index++]
|
||||
cumulativeY += vertices[index++]
|
||||
}
|
||||
return new Point.Point(cumulativeX / (vertices.length / 2), cumulativeY / (vertices.length / 2))
|
||||
}
|
||||
|
||||
static verticesFromNumVerts(numVertices, radius, centerX, centerY) {
|
||||
const result = []
|
||||
let arrayIndex = 0
|
||||
for (let i = 0; i < numVertices; i++) {
|
||||
const vertex = Utils.radialToCartesian(radius, (Math.PI / numVertices * 2 * i)).plus(new Point.Point(centerX, centerY))
|
||||
result[arrayIndex++] = vertex.x
|
||||
result[arrayIndex++] = vertex.y
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
static fromNumVertices(numVertices, radius = 1, centerX = 0, centerY = 0, rounding = CornerRounding.Unrounded, perVertexRounding = null) {
|
||||
return RoundedPolygon.fromVertices(this.verticesFromNumVerts(numVertices, radius, centerX, centerY), rounding, perVertexRounding, centerX, centerY)
|
||||
}
|
||||
|
||||
static fromVertices(vertices, rounding = CornerRounding.Unrounded, perVertexRounding = null, centerX = Number.MIN_SAFE_INTEGER, centerY = Number.MAX_SAFE_INTEGER) {
|
||||
const corners = []
|
||||
const n = vertices.length / 2
|
||||
const roundedCorners = []
|
||||
for (let i = 0; i < n; i++) {
|
||||
const vtxRounding = perVertexRounding?.[i] ?? rounding
|
||||
const prevIndex = ((i + n - 1) % n) * 2
|
||||
const nextIndex = ((i + 1) % n) * 2
|
||||
roundedCorners.push(
|
||||
new RoundedCorner.RoundedCorner(
|
||||
new Point.Point(vertices[prevIndex], vertices[prevIndex + 1]),
|
||||
new Point.Point(vertices[i * 2], vertices[i * 2 + 1]),
|
||||
new Point.Point(vertices[nextIndex], vertices[nextIndex + 1]),
|
||||
vtxRounding
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// For each side, check if we have enough space to do the cuts needed, and if not split
|
||||
// the available space, first for round cuts, then for smoothing if there is space left.
|
||||
// Each element in this list is a pair, that represent how much we can do of the cut for
|
||||
// the given side (side i goes from corner i to corner i+1), the elements of the pair are:
|
||||
// first is how much we can use of expectedRoundCut, second how much of expectedCut
|
||||
const cutAdjusts = Array.from({ length: n }).map((_, ix) => {
|
||||
const expectedRoundCut = roundedCorners[ix].expectedRoundCut + roundedCorners[(ix + 1) % n].expectedRoundCut
|
||||
const expectedCut = roundedCorners[ix].expectedCut + roundedCorners[(ix + 1) % n].expectedCut
|
||||
const vtxX = vertices[ix * 2]
|
||||
const vtxY = vertices[ix * 2 + 1]
|
||||
const nextVtxX = vertices[((ix + 1) % n) * 2]
|
||||
const nextVtxY = vertices[((ix + 1) % n) * 2 + 1]
|
||||
const sideSize = Utils.distance(vtxX - nextVtxX, vtxY - nextVtxY)
|
||||
|
||||
// Check expectedRoundCut first, and ensure we fulfill rounding needs first for
|
||||
// both corners before using space for smoothing
|
||||
if (expectedRoundCut > sideSize) {
|
||||
// Not enough room for fully rounding, see how much we can actually do.
|
||||
return { a: sideSize / expectedRoundCut, b: 0 }
|
||||
} else if (expectedCut > sideSize) {
|
||||
// We can do full rounding, but not full smoothing.
|
||||
return { a: 1, b: (sideSize - expectedRoundCut) / (expectedCut - expectedRoundCut) }
|
||||
} else {
|
||||
// There is enough room for rounding & smoothing.
|
||||
return { a: 1, b: 1 }
|
||||
}
|
||||
})
|
||||
|
||||
// Create and store list of beziers for each [potentially] rounded corner
|
||||
for (let i = 0; i < n; i++) {
|
||||
// allowedCuts[0] is for the side from the previous corner to this one,
|
||||
// allowedCuts[1] is for the side from this corner to the next one.
|
||||
const allowedCuts = []
|
||||
for(const delta of [0, 1]) {
|
||||
const { a: roundCutRatio, b: cutRatio } = cutAdjusts[(i + n - 1 + delta) % n]
|
||||
allowedCuts.push(
|
||||
roundedCorners[i].expectedRoundCut * roundCutRatio +
|
||||
(roundedCorners[i].expectedCut - roundedCorners[i].expectedRoundCut) * cutRatio
|
||||
)
|
||||
}
|
||||
corners.push(
|
||||
roundedCorners[i].getCubics(allowedCuts[0], allowedCuts[1])
|
||||
)
|
||||
}
|
||||
|
||||
const tempFeatures = []
|
||||
for (let i = 0; i < n; i++) {
|
||||
// Note that these indices are for pairs of values (points), they need to be
|
||||
// doubled to access the xy values in the vertices float array
|
||||
const prevVtxIndex = (i + n - 1) % n
|
||||
const nextVtxIndex = (i + 1) % n
|
||||
const currVertex = new Point.Point(vertices[i * 2], vertices[i * 2 + 1])
|
||||
const prevVertex = new Point.Point(vertices[prevVtxIndex * 2], vertices[prevVtxIndex * 2 + 1])
|
||||
const nextVertex = new Point.Point(vertices[nextVtxIndex * 2], vertices[nextVtxIndex * 2 + 1])
|
||||
const cnvx = Utils.convex(prevVertex, currVertex, nextVertex)
|
||||
tempFeatures.push(new Feature.Corner(corners[i], cnvx))
|
||||
tempFeatures.push(
|
||||
new Feature.Edge([Cubic.Cubic.straightLine(
|
||||
corners[i][corners[i].length - 1].anchor1X,
|
||||
corners[i][corners[i].length - 1].anchor1Y,
|
||||
corners[(i + 1) % n][0].anchor0X,
|
||||
corners[(i + 1) % n][0].anchor0Y,
|
||||
)])
|
||||
)
|
||||
}
|
||||
|
||||
let center
|
||||
if (centerX == Number.MIN_SAFE_INTEGER || centerY == Number.MIN_SAFE_INTEGER) {
|
||||
center = RoundedPolygon.calculateCenter(vertices)
|
||||
} else {
|
||||
center = new Point.Point(centerX, centerY)
|
||||
}
|
||||
|
||||
return RoundedPolygon.fromFeatures(tempFeatures, center.x, center.y)
|
||||
}
|
||||
|
||||
static fromFeatures(features, centerX, centerY) {
|
||||
const vertices = []
|
||||
for (const feature of features) {
|
||||
for (const cubic of feature.cubics) {
|
||||
vertices.push(cubic.anchor0X)
|
||||
vertices.push(cubic.anchor0Y)
|
||||
}
|
||||
}
|
||||
|
||||
if (Number.isNaN(centerX)) {
|
||||
centerX = this.calculateCenter(vertices).x
|
||||
}
|
||||
if (Number.isNaN(centerY)) {
|
||||
centerY = this.calculateCenter(vertices).y
|
||||
}
|
||||
|
||||
return new RoundedPolygon(features, new Point.Point(centerX, centerY))
|
||||
}
|
||||
|
||||
static circle(numVertices = 8, radius = 1, centerX = 0, centerY = 0) {
|
||||
// Half of the angle between two adjacent vertices on the polygon
|
||||
const theta = Math.PI / numVertices
|
||||
// Radius of the underlying RoundedPolygon object given the desired radius of the circle
|
||||
const polygonRadius = radius / Math.cos(theta)
|
||||
return RoundedPolygon.fromNumVertices(
|
||||
numVertices,
|
||||
polygonRadius,
|
||||
centerX,
|
||||
centerY,
|
||||
new CornerRounding.CornerRounding(radius)
|
||||
)
|
||||
}
|
||||
|
||||
static rectangle(width, height, rounding = CornerRounding.Unrounded, perVertexRounding = null, centerX = 0, centerY = 0) {
|
||||
const left = centerX - width / 2
|
||||
const top = centerY - height / 2
|
||||
const right = centerX + width / 2
|
||||
const bottom = centerY + height / 2
|
||||
|
||||
return RoundedPolygon.fromVertices([right, bottom, left, bottom, left, top, right, top], rounding, perVertexRounding, centerX, centerY)
|
||||
}
|
||||
|
||||
static star(numVerticesPerRadius, radius = 1, innerRadius = .5, rounding = CornerRounding.Unrounded, innerRounding = null, perVertexRounding = null, centerX = 0, centerY = 0) {
|
||||
let pvRounding = perVertexRounding
|
||||
// If no per-vertex rounding supplied and caller asked for inner rounding,
|
||||
// create per-vertex rounding list based on supplied outer/inner rounding parameters
|
||||
if (pvRounding == null && innerRounding != null) {
|
||||
pvRounding = Array.from({ length: numVerticesPerRadius * 2 }).flatMap(() => [rounding, innerRounding])
|
||||
}
|
||||
|
||||
return RoundedPolygon.fromVertices(RoundedPolygon.starVerticesFromNumVerts(numVerticesPerRadius, radius, innerRadius, centerX, centerY), rounding, perVertexRounding, centerX, centerY)
|
||||
}
|
||||
|
||||
static starVerticesFromNumVerts(numVerticesPerRadius, radius, innerRadius, centerX, centerY) {
|
||||
const result = []
|
||||
let arrayIndex = 0
|
||||
for (let i = 0; i < numVerticesPerRadius; i++) {
|
||||
let vertex = Utils.radialToCartesian(radius, (Math.PI / numVerticesPerRadius * 2 * i))
|
||||
result[arrayIndex++] = vertex.x + centerX
|
||||
result[arrayIndex++] = vertex.y + centerY
|
||||
vertex = Utils.radialToCartesian(innerRadius, (Math.PI / numVerticesPerRadius * (2 * i + 1)))
|
||||
result[arrayIndex++] = vertex.x + centerX
|
||||
result[arrayIndex++] = vertex.y + centerY
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
.pragma library
|
||||
.import "point.js" as PointModule
|
||||
|
||||
var Point = PointModule.Point;
|
||||
var DistanceEpsilon = 1e-4;
|
||||
var AngleEpsilon = 1e-6;
|
||||
|
||||
/**
|
||||
* @param {Point} previous
|
||||
* @param {Point} current
|
||||
* @param {Point} next
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function convex(previous, current, next) {
|
||||
return (current.minus(previous)).clockwise(next.minus(current));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {float} start
|
||||
* @param {float} stop
|
||||
* @param {float} fraction
|
||||
* @returns {float}
|
||||
*/
|
||||
function interpolate(start, stop, fraction) {
|
||||
return (1 - fraction) * start + fraction * stop;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {float} x
|
||||
* @param {float} y
|
||||
* @returns {Point}
|
||||
*/
|
||||
function directionVector(x, y) {
|
||||
const d = distance(x, y);
|
||||
return new Point(x / d, y / d);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {float} x
|
||||
* @param {float} y
|
||||
* @returns {float}
|
||||
*/
|
||||
function distance(x, y) {
|
||||
return Math.sqrt(x * x + y * y);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {float} x
|
||||
* @param {float} y
|
||||
* @returns {float}
|
||||
*/
|
||||
function distanceSquared(x, y) {
|
||||
return x * x + y * y;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {float} radius
|
||||
* @param {float} angleRadians
|
||||
* @param {Point} [center]
|
||||
* @returns {Point}
|
||||
*/
|
||||
function radialToCartesian(radius, angleRadians, center = new Point(0, 0)) {
|
||||
return new Point(Math.cos(angleRadians), Math.sin(angleRadians))
|
||||
.times(radius)
|
||||
.plus(center);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {float} value
|
||||
* @param {float|object} min
|
||||
* @param {float} [max]
|
||||
* @returns {float}
|
||||
*/
|
||||
function coerceIn(value, min, max) {
|
||||
if (max === undefined) {
|
||||
if (typeof min === 'object' && 'start' in min && 'endInclusive' in min) {
|
||||
return Math.max(min.start, Math.min(min.endInclusive, value));
|
||||
}
|
||||
throw new Error("Invalid arguments for coerceIn");
|
||||
}
|
||||
|
||||
const [actualMin, actualMax] = min <= max ? [min, max] : [max, min];
|
||||
return Math.max(actualMin, Math.min(actualMax, value));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {float} value
|
||||
* @param {float} mod
|
||||
* @returns {float}
|
||||
*/
|
||||
function positiveModulo(value, mod) {
|
||||
return ((value % mod) + mod) % mod;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
pragma Singleton
|
||||
import Quickshell
|
||||
|
||||
// From github.com/end-4/dots-hyprland with modifications
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
function colorWithHueOf(color1, color2) {
|
||||
var c1 = Qt.color(color1);
|
||||
var c2 = Qt.color(color2);
|
||||
var hue = c2.hsvHue;
|
||||
var sat = c1.hsvSaturation;
|
||||
var val = c1.hsvValue;
|
||||
var alpha = c1.a;
|
||||
return Qt.hsva(hue, sat, val, alpha);
|
||||
}
|
||||
|
||||
function colorWithSaturationOf(color1, color2) {
|
||||
var c1 = Qt.color(color1);
|
||||
var c2 = Qt.color(color2);
|
||||
var hue = c1.hsvHue;
|
||||
var sat = c2.hsvSaturation;
|
||||
var val = c1.hsvValue;
|
||||
var alpha = c1.a;
|
||||
return Qt.hsva(hue, sat, val, alpha);
|
||||
}
|
||||
|
||||
function colorWithLightness(color, lightness) {
|
||||
var c = Qt.color(color);
|
||||
return Qt.hsla(c.hslHue, c.hslSaturation, lightness, c.a);
|
||||
}
|
||||
|
||||
function colorWithLightnessOf(color1, color2) {
|
||||
var c2 = Qt.color(color2);
|
||||
return colorWithLightness(color1, c2.hslLightness);
|
||||
}
|
||||
|
||||
function adaptToAccent(color1, color2) {
|
||||
var c1 = Qt.color(color1);
|
||||
var c2 = Qt.color(color2);
|
||||
var hue = c2.hslHue;
|
||||
var sat = c2.hslSaturation;
|
||||
var light = c1.hslLightness;
|
||||
var alpha = c1.a;
|
||||
return Qt.hsla(hue, sat, light, alpha);
|
||||
}
|
||||
|
||||
function mix(color1, color2, percentage = 0.5) {
|
||||
var c1 = Qt.color(color1);
|
||||
var c2 = Qt.color(color2);
|
||||
return Qt.rgba(
|
||||
percentage * c1.r + (1 - percentage) * c2.r,
|
||||
percentage * c1.g + (1 - percentage) * c2.g,
|
||||
percentage * c1.b + (1 - percentage) * c2.b,
|
||||
percentage * c1.a + (1 - percentage) * c2.a
|
||||
);
|
||||
}
|
||||
|
||||
function transparentize(color, percentage = 1) {
|
||||
var c = Qt.color(color);
|
||||
return Qt.rgba(c.r, c.g, c.b, c.a * (1 - percentage));
|
||||
}
|
||||
|
||||
function applyAlpha(color, alpha) {
|
||||
var c = Qt.color(color);
|
||||
var a = Math.max(0, Math.min(1, alpha));
|
||||
return Qt.rgba(c.r, c.g, c.b, a);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
pragma Singleton
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import qs.services
|
||||
|
||||
Singleton {
|
||||
// Prefer Compositor scales because niri and hyprland have diffrent scaling factors
|
||||
function scaledWidth(ratio) {
|
||||
return Compositor.screenW * ratio / Compositor.screenScale
|
||||
}
|
||||
|
||||
function scaledHeight(ratio) {
|
||||
return Compositor.screenH * ratio / Compositor.screenScale
|
||||
}
|
||||
}
|
||||
106
.config/quickshell/nucleus-shell/modules/functions/FileUtils.qml
Normal file
106
.config/quickshell/nucleus-shell/modules/functions/FileUtils.qml
Normal file
@@ -0,0 +1,106 @@
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
pragma Singleton
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
function resolveIcon(className) {
|
||||
if (!className || className.length === 0)
|
||||
return "";
|
||||
|
||||
const original = className;
|
||||
const normalized = className.toLowerCase();
|
||||
// 1. Exact icon name
|
||||
if (Quickshell.iconPath(original, true).length > 0)
|
||||
return original;
|
||||
|
||||
// 2. Normalized guess
|
||||
if (Quickshell.iconPath(normalized, true).length > 0)
|
||||
return normalized;
|
||||
|
||||
// 3. Dashed guess
|
||||
const dashed = normalized.replace(/\s+/g, "-");
|
||||
if (Quickshell.iconPath(dashed, true).length > 0)
|
||||
return dashed;
|
||||
|
||||
// 4. Extension guess
|
||||
const ext = original.split(".").pop().toLowerCase();
|
||||
if (Quickshell.iconPath(ext, true).length > 0)
|
||||
return ext;
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
function trimFileProtocol(str) {
|
||||
let s = str;
|
||||
if (typeof s !== "string")
|
||||
s = str.toString();
|
||||
|
||||
// Convert to string if it's an url or whatever
|
||||
return s.startsWith("file://") ? s.slice(7) : s;
|
||||
}
|
||||
|
||||
function isVideo(path) {
|
||||
if (!path)
|
||||
return false;
|
||||
|
||||
// Convert QUrl → string if needed
|
||||
let p = path.toString ? path.toString() : path;
|
||||
// Strip file://
|
||||
if (p.startsWith("file://"))
|
||||
p = p.replace("file://", "");
|
||||
|
||||
const ext = p.split(".").pop().toLowerCase();
|
||||
return ["mp4", "mkv", "webm", "mov", "avi", "m4v"].includes(ext);
|
||||
}
|
||||
|
||||
function createFile(filePath, callback) {
|
||||
if (!filePath)
|
||||
return ;
|
||||
|
||||
let p = Qt.createQmlObject('import QtQuick; import Quickshell.Io; Process {}', root);
|
||||
p.command = ["touch", filePath];
|
||||
p.onExited.connect(function() {
|
||||
console.debug("Created file:", filePath, "exit code:", p.exitCode);
|
||||
p.destroy();
|
||||
if (callback)
|
||||
callback(true);
|
||||
|
||||
});
|
||||
p.running = true;
|
||||
}
|
||||
|
||||
function removeFile(filePath, callback) {
|
||||
if (!filePath)
|
||||
return ;
|
||||
|
||||
let p = Qt.createQmlObject('import QtQuick; import Quickshell.Io; Process {}', root);
|
||||
p.command = ["rm", "-f", filePath];
|
||||
p.onExited.connect(function() {
|
||||
console.debug("Removed file:", filePath, "exit code:", p.exitCode);
|
||||
p.destroy();
|
||||
if (callback)
|
||||
callback(true);
|
||||
|
||||
});
|
||||
p.running = true;
|
||||
}
|
||||
|
||||
function renameFile(oldPath, newPath, callback) {
|
||||
if (!oldPath || !newPath || oldPath === newPath)
|
||||
return ;
|
||||
|
||||
let p = Qt.createQmlObject('import QtQuick; import Quickshell.Io; Process {}', root);
|
||||
p.command = ["mv", oldPath, newPath];
|
||||
p.onExited.connect(function() {
|
||||
console.debug("Renamed file:", oldPath, "→", newPath, "exit code:", p.exitCode);
|
||||
p.destroy();
|
||||
if (callback)
|
||||
callback(true);
|
||||
|
||||
});
|
||||
p.running = true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
pragma Singleton
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
function shortText(str, len = 25) {
|
||||
if (!str)
|
||||
return ""
|
||||
return str.length > len ? str.slice(0, len) + "..." : str
|
||||
}
|
||||
|
||||
function verticalize(text) {
|
||||
return text.split("").join("\n")
|
||||
}
|
||||
|
||||
function markdownToHtml(md) {
|
||||
if (!md) return "";
|
||||
|
||||
let html = md
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/\*\*(.*?)\*\*/g, "<b>$1</b>") // bold
|
||||
.replace(/\*(.*?)\*/g, "<i>$1</i>") // italic
|
||||
.replace(/`([^`]+)`/g, "<code>$1</code>") // inline code
|
||||
.replace(/^### (.*)$/gm, "<h3>$1</h3>") // headers
|
||||
.replace(/```([\s\S]+?)```/g, '<pre style="font-family:monospace">$1</pre>') // code blocks
|
||||
.replace(/^## (.*)$/gm, "<h2>$1</h2>")
|
||||
.replace(/^# (.*)$/gm, "<h1>$1</h1>")
|
||||
.replace(/^- (.*)$/gm, "<li>$1</li>"); // simple lists
|
||||
|
||||
// Wrap list items in <ul> without `s` flag
|
||||
html = html.replace(/(<li>[\s\S]*?<\/li>)/g, "<ul>$1</ul>");
|
||||
|
||||
// Replace newlines with <br> for normal text
|
||||
html = html.replace(/\n/g, "<br>");
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,679 @@
|
||||
.pragma library
|
||||
|
||||
var single = (search, target) => {
|
||||
if(!search || !target) return NULL
|
||||
|
||||
var preparedSearch = getPreparedSearch(search)
|
||||
if(!isPrepared(target)) target = getPrepared(target)
|
||||
|
||||
var searchBitflags = preparedSearch.bitflags
|
||||
if((searchBitflags & target._bitflags) !== searchBitflags) return NULL
|
||||
|
||||
return algorithm(preparedSearch, target)
|
||||
}
|
||||
|
||||
var go = (search, targets, options) => {
|
||||
if(!search) return options?.all ? all(targets, options) : noResults
|
||||
|
||||
var preparedSearch = getPreparedSearch(search)
|
||||
var searchBitflags = preparedSearch.bitflags
|
||||
var containsSpace = preparedSearch.containsSpace
|
||||
|
||||
var threshold = denormalizeScore( options?.threshold || 0 )
|
||||
var limit = options?.limit || INFINITY
|
||||
|
||||
var resultsLen = 0; var limitedCount = 0
|
||||
var targetsLen = targets.length
|
||||
|
||||
function push_result(result) {
|
||||
if(resultsLen < limit) { q.add(result); ++resultsLen }
|
||||
else {
|
||||
++limitedCount
|
||||
if(result._score > q.peek()._score) q.replaceTop(result)
|
||||
}
|
||||
}
|
||||
|
||||
// This code is copy/pasted 3 times for performance reasons [options.key, options.keys, no keys]
|
||||
|
||||
// options.key
|
||||
if(options?.key) {
|
||||
var key = options.key
|
||||
for(var i = 0; i < targetsLen; ++i) { var obj = targets[i]
|
||||
var target = getValue(obj, key)
|
||||
if(!target) continue
|
||||
if(!isPrepared(target)) target = getPrepared(target)
|
||||
|
||||
if((searchBitflags & target._bitflags) !== searchBitflags) continue
|
||||
var result = algorithm(preparedSearch, target)
|
||||
if(result === NULL) continue
|
||||
if(result._score < threshold) continue
|
||||
|
||||
result.obj = obj
|
||||
push_result(result)
|
||||
}
|
||||
|
||||
// options.keys
|
||||
} else if(options?.keys) {
|
||||
var keys = options.keys
|
||||
var keysLen = keys.length
|
||||
|
||||
outer: for(var i = 0; i < targetsLen; ++i) { var obj = targets[i]
|
||||
|
||||
{ // early out based on bitflags
|
||||
var keysBitflags = 0
|
||||
for (var keyI = 0; keyI < keysLen; ++keyI) {
|
||||
var key = keys[keyI]
|
||||
var target = getValue(obj, key)
|
||||
if(!target) { tmpTargets[keyI] = noTarget; continue }
|
||||
if(!isPrepared(target)) target = getPrepared(target)
|
||||
tmpTargets[keyI] = target
|
||||
|
||||
keysBitflags |= target._bitflags
|
||||
}
|
||||
|
||||
if((searchBitflags & keysBitflags) !== searchBitflags) continue
|
||||
}
|
||||
|
||||
if(containsSpace) for(let i=0; i<preparedSearch.spaceSearches.length; i++) keysSpacesBestScores[i] = NEGATIVE_INFINITY
|
||||
|
||||
for (var keyI = 0; keyI < keysLen; ++keyI) {
|
||||
target = tmpTargets[keyI]
|
||||
if(target === noTarget) { tmpResults[keyI] = noTarget; continue }
|
||||
|
||||
tmpResults[keyI] = algorithm(preparedSearch, target, /*allowSpaces=*/false, /*allowPartialMatch=*/containsSpace)
|
||||
if(tmpResults[keyI] === NULL) { tmpResults[keyI] = noTarget; continue }
|
||||
|
||||
// todo: this seems weird and wrong. like what if our first match wasn't good. this should just replace it instead of averaging with it
|
||||
// if our second match isn't good we ignore it instead of averaging with it
|
||||
if(containsSpace) for(let i=0; i<preparedSearch.spaceSearches.length; i++) {
|
||||
if(allowPartialMatchScores[i] > -1000) {
|
||||
if(keysSpacesBestScores[i] > NEGATIVE_INFINITY) {
|
||||
var tmp = (keysSpacesBestScores[i] + allowPartialMatchScores[i]) / 4/*bonus score for having multiple matches*/
|
||||
if(tmp > keysSpacesBestScores[i]) keysSpacesBestScores[i] = tmp
|
||||
}
|
||||
}
|
||||
if(allowPartialMatchScores[i] > keysSpacesBestScores[i]) keysSpacesBestScores[i] = allowPartialMatchScores[i]
|
||||
}
|
||||
}
|
||||
|
||||
if(containsSpace) {
|
||||
for(let i=0; i<preparedSearch.spaceSearches.length; i++) { if(keysSpacesBestScores[i] === NEGATIVE_INFINITY) continue outer }
|
||||
} else {
|
||||
var hasAtLeast1Match = false
|
||||
for(let i=0; i < keysLen; i++) { if(tmpResults[i]._score !== NEGATIVE_INFINITY) { hasAtLeast1Match = true; break } }
|
||||
if(!hasAtLeast1Match) continue
|
||||
}
|
||||
|
||||
var objResults = new KeysResult(keysLen)
|
||||
for(let i=0; i < keysLen; i++) { objResults[i] = tmpResults[i] }
|
||||
|
||||
if(containsSpace) {
|
||||
var score = 0
|
||||
for(let i=0; i<preparedSearch.spaceSearches.length; i++) score += keysSpacesBestScores[i]
|
||||
} else {
|
||||
// todo could rewrite this scoring to be more similar to when there's spaces
|
||||
// if we match multiple keys give us bonus points
|
||||
var score = NEGATIVE_INFINITY
|
||||
for(let i=0; i<keysLen; i++) {
|
||||
var result = objResults[i]
|
||||
if(result._score > -1000) {
|
||||
if(score > NEGATIVE_INFINITY) {
|
||||
var tmp = (score + result._score) / 4/*bonus score for having multiple matches*/
|
||||
if(tmp > score) score = tmp
|
||||
}
|
||||
}
|
||||
if(result._score > score) score = result._score
|
||||
}
|
||||
}
|
||||
|
||||
objResults.obj = obj
|
||||
objResults._score = score
|
||||
if(options?.scoreFn) {
|
||||
score = options.scoreFn(objResults)
|
||||
if(!score) continue
|
||||
score = denormalizeScore(score)
|
||||
objResults._score = score
|
||||
}
|
||||
|
||||
if(score < threshold) continue
|
||||
push_result(objResults)
|
||||
}
|
||||
|
||||
// no keys
|
||||
} else {
|
||||
for(var i = 0; i < targetsLen; ++i) { var target = targets[i]
|
||||
if(!target) continue
|
||||
if(!isPrepared(target)) target = getPrepared(target)
|
||||
|
||||
if((searchBitflags & target._bitflags) !== searchBitflags) continue
|
||||
var result = algorithm(preparedSearch, target)
|
||||
if(result === NULL) continue
|
||||
if(result._score < threshold) continue
|
||||
|
||||
push_result(result)
|
||||
}
|
||||
}
|
||||
|
||||
if(resultsLen === 0) return noResults
|
||||
var results = new Array(resultsLen)
|
||||
for(var i = resultsLen - 1; i >= 0; --i) results[i] = q.poll()
|
||||
results.total = resultsLen + limitedCount
|
||||
return results
|
||||
}
|
||||
|
||||
|
||||
// this is written as 1 function instead of 2 for minification. perf seems fine ...
|
||||
// except when minified. the perf is very slow
|
||||
var highlight = (result, open='<b>', close='</b>') => {
|
||||
var callback = typeof open === 'function' ? open : undefined
|
||||
|
||||
var target = result.target
|
||||
var targetLen = target.length
|
||||
var indexes = result.indexes
|
||||
var highlighted = ''
|
||||
var matchI = 0
|
||||
var indexesI = 0
|
||||
var opened = false
|
||||
var parts = []
|
||||
|
||||
for(var i = 0; i < targetLen; ++i) { var char = target[i]
|
||||
if(indexes[indexesI] === i) {
|
||||
++indexesI
|
||||
if(!opened) { opened = true
|
||||
if(callback) {
|
||||
parts.push(highlighted); highlighted = ''
|
||||
} else {
|
||||
highlighted += open
|
||||
}
|
||||
}
|
||||
|
||||
if(indexesI === indexes.length) {
|
||||
if(callback) {
|
||||
highlighted += char
|
||||
parts.push(callback(highlighted, matchI++)); highlighted = ''
|
||||
parts.push(target.substr(i+1))
|
||||
} else {
|
||||
highlighted += char + close + target.substr(i+1)
|
||||
}
|
||||
break
|
||||
}
|
||||
} else {
|
||||
if(opened) { opened = false
|
||||
if(callback) {
|
||||
parts.push(callback(highlighted, matchI++)); highlighted = ''
|
||||
} else {
|
||||
highlighted += close
|
||||
}
|
||||
}
|
||||
}
|
||||
highlighted += char
|
||||
}
|
||||
|
||||
return callback ? parts : highlighted
|
||||
}
|
||||
|
||||
|
||||
var prepare = (target) => {
|
||||
if(typeof target === 'number') target = ''+target
|
||||
else if(typeof target !== 'string') target = ''
|
||||
var info = prepareLowerInfo(target)
|
||||
return new_result(target, {_targetLower:info._lower, _targetLowerCodes:info.lowerCodes, _bitflags:info.bitflags})
|
||||
}
|
||||
|
||||
var cleanup = () => { preparedCache.clear(); preparedSearchCache.clear() }
|
||||
|
||||
|
||||
// Below this point is only internal code
|
||||
// Below this point is only internal code
|
||||
// Below this point is only internal code
|
||||
// Below this point is only internal code
|
||||
|
||||
|
||||
class Result {
|
||||
get ['indexes']() { return this._indexes.slice(0, this._indexes.len).sort((a,b)=>a-b) }
|
||||
set ['indexes'](indexes) { return this._indexes = indexes }
|
||||
['highlight'](open, close) { return highlight(this, open, close) }
|
||||
get ['score']() { return normalizeScore(this._score) }
|
||||
set ['score'](score) { this._score = denormalizeScore(score) }
|
||||
}
|
||||
|
||||
class KeysResult extends Array {
|
||||
get ['score']() { return normalizeScore(this._score) }
|
||||
set ['score'](score) { this._score = denormalizeScore(score) }
|
||||
}
|
||||
|
||||
var new_result = (target, options) => {
|
||||
const result = new Result()
|
||||
result['target'] = target
|
||||
result['obj'] = options.obj ?? NULL
|
||||
result._score = options._score ?? NEGATIVE_INFINITY
|
||||
result._indexes = options._indexes ?? []
|
||||
result._targetLower = options._targetLower ?? ''
|
||||
result._targetLowerCodes = options._targetLowerCodes ?? NULL
|
||||
result._nextBeginningIndexes = options._nextBeginningIndexes ?? NULL
|
||||
result._bitflags = options._bitflags ?? 0
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
var normalizeScore = score => {
|
||||
if(score === NEGATIVE_INFINITY) return 0
|
||||
if(score > 1) return score
|
||||
return Math.E ** ( ((-score + 1)**.04307 - 1) * -2)
|
||||
}
|
||||
var denormalizeScore = normalizedScore => {
|
||||
if(normalizedScore === 0) return NEGATIVE_INFINITY
|
||||
if(normalizedScore > 1) return normalizedScore
|
||||
return 1 - Math.pow((Math.log(normalizedScore) / -2 + 1), 1 / 0.04307)
|
||||
}
|
||||
|
||||
|
||||
var prepareSearch = (search) => {
|
||||
if(typeof search === 'number') search = ''+search
|
||||
else if(typeof search !== 'string') search = ''
|
||||
search = search.trim()
|
||||
var info = prepareLowerInfo(search)
|
||||
|
||||
var spaceSearches = []
|
||||
if(info.containsSpace) {
|
||||
var searches = search.split(/\s+/)
|
||||
searches = [...new Set(searches)] // distinct
|
||||
for(var i=0; i<searches.length; i++) {
|
||||
if(searches[i] === '') continue
|
||||
var _info = prepareLowerInfo(searches[i])
|
||||
spaceSearches.push({lowerCodes:_info.lowerCodes, _lower:searches[i].toLowerCase(), containsSpace:false})
|
||||
}
|
||||
}
|
||||
|
||||
return {lowerCodes: info.lowerCodes, _lower: info._lower, containsSpace: info.containsSpace, bitflags: info.bitflags, spaceSearches: spaceSearches}
|
||||
}
|
||||
|
||||
|
||||
|
||||
var getPrepared = (target) => {
|
||||
if(target.length > 999) return prepare(target) // don't cache huge targets
|
||||
var targetPrepared = preparedCache.get(target)
|
||||
if(targetPrepared !== undefined) return targetPrepared
|
||||
targetPrepared = prepare(target)
|
||||
preparedCache.set(target, targetPrepared)
|
||||
return targetPrepared
|
||||
}
|
||||
var getPreparedSearch = (search) => {
|
||||
if(search.length > 999) return prepareSearch(search) // don't cache huge searches
|
||||
var searchPrepared = preparedSearchCache.get(search)
|
||||
if(searchPrepared !== undefined) return searchPrepared
|
||||
searchPrepared = prepareSearch(search)
|
||||
preparedSearchCache.set(search, searchPrepared)
|
||||
return searchPrepared
|
||||
}
|
||||
|
||||
|
||||
var all = (targets, options) => {
|
||||
var results = []; results.total = targets.length // this total can be wrong if some targets are skipped
|
||||
|
||||
var limit = options?.limit || INFINITY
|
||||
|
||||
if(options?.key) {
|
||||
for(var i=0;i<targets.length;i++) { var obj = targets[i]
|
||||
var target = getValue(obj, options.key)
|
||||
if(target == NULL) continue
|
||||
if(!isPrepared(target)) target = getPrepared(target)
|
||||
var result = new_result(target.target, {_score: target._score, obj: obj})
|
||||
results.push(result); if(results.length >= limit) return results
|
||||
}
|
||||
} else if(options?.keys) {
|
||||
for(var i=0;i<targets.length;i++) { var obj = targets[i]
|
||||
var objResults = new KeysResult(options.keys.length)
|
||||
for (var keyI = options.keys.length - 1; keyI >= 0; --keyI) {
|
||||
var target = getValue(obj, options.keys[keyI])
|
||||
if(!target) { objResults[keyI] = noTarget; continue }
|
||||
if(!isPrepared(target)) target = getPrepared(target)
|
||||
target._score = NEGATIVE_INFINITY
|
||||
target._indexes.len = 0
|
||||
objResults[keyI] = target
|
||||
}
|
||||
objResults.obj = obj
|
||||
objResults._score = NEGATIVE_INFINITY
|
||||
results.push(objResults); if(results.length >= limit) return results
|
||||
}
|
||||
} else {
|
||||
for(var i=0;i<targets.length;i++) { var target = targets[i]
|
||||
if(target == NULL) continue
|
||||
if(!isPrepared(target)) target = getPrepared(target)
|
||||
target._score = NEGATIVE_INFINITY
|
||||
target._indexes.len = 0
|
||||
results.push(target); if(results.length >= limit) return results
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
|
||||
var algorithm = (preparedSearch, prepared, allowSpaces=false, allowPartialMatch=false) => {
|
||||
if(allowSpaces===false && preparedSearch.containsSpace) return algorithmSpaces(preparedSearch, prepared, allowPartialMatch)
|
||||
|
||||
var searchLower = preparedSearch._lower
|
||||
var searchLowerCodes = preparedSearch.lowerCodes
|
||||
var searchLowerCode = searchLowerCodes[0]
|
||||
var targetLowerCodes = prepared._targetLowerCodes
|
||||
var searchLen = searchLowerCodes.length
|
||||
var targetLen = targetLowerCodes.length
|
||||
var searchI = 0 // where we at
|
||||
var targetI = 0 // where you at
|
||||
var matchesSimpleLen = 0
|
||||
|
||||
// very basic fuzzy match; to remove non-matching targets ASAP!
|
||||
// walk through target. find sequential matches.
|
||||
// if all chars aren't found then exit
|
||||
for(;;) {
|
||||
var isMatch = searchLowerCode === targetLowerCodes[targetI]
|
||||
if(isMatch) {
|
||||
matchesSimple[matchesSimpleLen++] = targetI
|
||||
++searchI; if(searchI === searchLen) break
|
||||
searchLowerCode = searchLowerCodes[searchI]
|
||||
}
|
||||
++targetI; if(targetI >= targetLen) return NULL // Failed to find searchI
|
||||
}
|
||||
|
||||
var searchI = 0
|
||||
var successStrict = false
|
||||
var matchesStrictLen = 0
|
||||
|
||||
var nextBeginningIndexes = prepared._nextBeginningIndexes
|
||||
if(nextBeginningIndexes === NULL) nextBeginningIndexes = prepared._nextBeginningIndexes = prepareNextBeginningIndexes(prepared.target)
|
||||
targetI = matchesSimple[0]===0 ? 0 : nextBeginningIndexes[matchesSimple[0]-1]
|
||||
|
||||
// Our target string successfully matched all characters in sequence!
|
||||
// Let's try a more advanced and strict test to improve the score
|
||||
// only count it as a match if it's consecutive or a beginning character!
|
||||
var backtrackCount = 0
|
||||
if(targetI !== targetLen) for(;;) {
|
||||
if(targetI >= targetLen) {
|
||||
// We failed to find a good spot for this search char, go back to the previous search char and force it forward
|
||||
if(searchI <= 0) break // We failed to push chars forward for a better match
|
||||
|
||||
++backtrackCount; if(backtrackCount > 200) break // exponential backtracking is taking too long, just give up and return a bad match
|
||||
|
||||
--searchI
|
||||
var lastMatch = matchesStrict[--matchesStrictLen]
|
||||
targetI = nextBeginningIndexes[lastMatch]
|
||||
|
||||
} else {
|
||||
var isMatch = searchLowerCodes[searchI] === targetLowerCodes[targetI]
|
||||
if(isMatch) {
|
||||
matchesStrict[matchesStrictLen++] = targetI
|
||||
++searchI; if(searchI === searchLen) { successStrict = true; break }
|
||||
++targetI
|
||||
} else {
|
||||
targetI = nextBeginningIndexes[targetI]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check if it's a substring match
|
||||
var substringIndex = searchLen <= 1 ? -1 : prepared._targetLower.indexOf(searchLower, matchesSimple[0]) // perf: this is slow
|
||||
var isSubstring = !!~substringIndex
|
||||
var isSubstringBeginning = !isSubstring ? false : substringIndex===0 || prepared._nextBeginningIndexes[substringIndex-1] === substringIndex
|
||||
|
||||
// if it's a substring match but not at a beginning index, let's try to find a substring starting at a beginning index for a better score
|
||||
if(isSubstring && !isSubstringBeginning) {
|
||||
for(var i=0; i<nextBeginningIndexes.length; i=nextBeginningIndexes[i]) {
|
||||
if(i <= substringIndex) continue
|
||||
|
||||
for(var s=0; s<searchLen; s++) if(searchLowerCodes[s] !== prepared._targetLowerCodes[i+s]) break
|
||||
if(s === searchLen) { substringIndex = i; isSubstringBeginning = true; break }
|
||||
}
|
||||
}
|
||||
|
||||
// tally up the score & keep track of matches for highlighting later
|
||||
// if it's a simple match, we'll switch to a substring match if a substring exists
|
||||
// if it's a strict match, we'll switch to a substring match only if that's a better score
|
||||
|
||||
var calculateScore = matches => {
|
||||
var score = 0
|
||||
|
||||
var extraMatchGroupCount = 0
|
||||
for(var i = 1; i < searchLen; ++i) {
|
||||
if(matches[i] - matches[i-1] !== 1) {score -= matches[i]; ++extraMatchGroupCount}
|
||||
}
|
||||
var unmatchedDistance = matches[searchLen-1] - matches[0] - (searchLen-1)
|
||||
|
||||
score -= (12+unmatchedDistance) * extraMatchGroupCount // penality for more groups
|
||||
|
||||
if(matches[0] !== 0) score -= matches[0]*matches[0]*.2 // penality for not starting near the beginning
|
||||
|
||||
if(!successStrict) {
|
||||
score *= 1000
|
||||
} else {
|
||||
// successStrict on a target with too many beginning indexes loses points for being a bad target
|
||||
var uniqueBeginningIndexes = 1
|
||||
for(var i = nextBeginningIndexes[0]; i < targetLen; i=nextBeginningIndexes[i]) ++uniqueBeginningIndexes
|
||||
|
||||
if(uniqueBeginningIndexes > 24) score *= (uniqueBeginningIndexes-24)*10 // quite arbitrary numbers here ...
|
||||
}
|
||||
|
||||
score -= (targetLen - searchLen)/2 // penality for longer targets
|
||||
|
||||
if(isSubstring) score /= 1+searchLen*searchLen*1 // bonus for being a full substring
|
||||
if(isSubstringBeginning) score /= 1+searchLen*searchLen*1 // bonus for substring starting on a beginningIndex
|
||||
|
||||
score -= (targetLen - searchLen)/2 // penality for longer targets
|
||||
|
||||
return score
|
||||
}
|
||||
|
||||
if(!successStrict) {
|
||||
if(isSubstring) for(var i=0; i<searchLen; ++i) matchesSimple[i] = substringIndex+i // at this point it's safe to overwrite matchehsSimple with substr matches
|
||||
var matchesBest = matchesSimple
|
||||
var score = calculateScore(matchesBest)
|
||||
} else {
|
||||
if(isSubstringBeginning) {
|
||||
for(var i=0; i<searchLen; ++i) matchesSimple[i] = substringIndex+i // at this point it's safe to overwrite matchehsSimple with substr matches
|
||||
var matchesBest = matchesSimple
|
||||
var score = calculateScore(matchesSimple)
|
||||
} else {
|
||||
var matchesBest = matchesStrict
|
||||
var score = calculateScore(matchesStrict)
|
||||
}
|
||||
}
|
||||
|
||||
prepared._score = score
|
||||
|
||||
for(var i = 0; i < searchLen; ++i) prepared._indexes[i] = matchesBest[i]
|
||||
prepared._indexes.len = searchLen
|
||||
|
||||
const result = new Result()
|
||||
result.target = prepared.target
|
||||
result._score = prepared._score
|
||||
result._indexes = prepared._indexes
|
||||
return result
|
||||
}
|
||||
var algorithmSpaces = (preparedSearch, target, allowPartialMatch) => {
|
||||
var seen_indexes = new Set()
|
||||
var score = 0
|
||||
var result = NULL
|
||||
|
||||
var first_seen_index_last_search = 0
|
||||
var searches = preparedSearch.spaceSearches
|
||||
var searchesLen = searches.length
|
||||
var changeslen = 0
|
||||
|
||||
// Return _nextBeginningIndexes back to its normal state
|
||||
var resetNextBeginningIndexes = () => {
|
||||
for(let i=changeslen-1; i>=0; i--) target._nextBeginningIndexes[nextBeginningIndexesChanges[i*2 + 0]] = nextBeginningIndexesChanges[i*2 + 1]
|
||||
}
|
||||
|
||||
var hasAtLeast1Match = false
|
||||
for(var i=0; i<searchesLen; ++i) {
|
||||
allowPartialMatchScores[i] = NEGATIVE_INFINITY
|
||||
var search = searches[i]
|
||||
|
||||
result = algorithm(search, target)
|
||||
if(allowPartialMatch) {
|
||||
if(result === NULL) continue
|
||||
hasAtLeast1Match = true
|
||||
} else {
|
||||
if(result === NULL) {resetNextBeginningIndexes(); return NULL}
|
||||
}
|
||||
|
||||
// if not the last search, we need to mutate _nextBeginningIndexes for the next search
|
||||
var isTheLastSearch = i === searchesLen - 1
|
||||
if(!isTheLastSearch) {
|
||||
var indexes = result._indexes
|
||||
|
||||
var indexesIsConsecutiveSubstring = true
|
||||
for(let i=0; i<indexes.len-1; i++) {
|
||||
if(indexes[i+1] - indexes[i] !== 1) {
|
||||
indexesIsConsecutiveSubstring = false; break;
|
||||
}
|
||||
}
|
||||
|
||||
if(indexesIsConsecutiveSubstring) {
|
||||
var newBeginningIndex = indexes[indexes.len-1] + 1
|
||||
var toReplace = target._nextBeginningIndexes[newBeginningIndex-1]
|
||||
for(let i=newBeginningIndex-1; i>=0; i--) {
|
||||
if(toReplace !== target._nextBeginningIndexes[i]) break
|
||||
target._nextBeginningIndexes[i] = newBeginningIndex
|
||||
nextBeginningIndexesChanges[changeslen*2 + 0] = i
|
||||
nextBeginningIndexesChanges[changeslen*2 + 1] = toReplace
|
||||
changeslen++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
score += result._score / searchesLen
|
||||
allowPartialMatchScores[i] = result._score / searchesLen
|
||||
|
||||
// dock points based on order otherwise "c man" returns Manifest.cpp instead of CheatManager.h
|
||||
if(result._indexes[0] < first_seen_index_last_search) {
|
||||
score -= (first_seen_index_last_search - result._indexes[0]) * 2
|
||||
}
|
||||
first_seen_index_last_search = result._indexes[0]
|
||||
|
||||
for(var j=0; j<result._indexes.len; ++j) seen_indexes.add(result._indexes[j])
|
||||
}
|
||||
|
||||
if(allowPartialMatch && !hasAtLeast1Match) return NULL
|
||||
|
||||
resetNextBeginningIndexes()
|
||||
|
||||
// allows a search with spaces that's an exact substring to score well
|
||||
var allowSpacesResult = algorithm(preparedSearch, target, /*allowSpaces=*/true)
|
||||
if(allowSpacesResult !== NULL && allowSpacesResult._score > score) {
|
||||
if(allowPartialMatch) {
|
||||
for(var i=0; i<searchesLen; ++i) {
|
||||
allowPartialMatchScores[i] = allowSpacesResult._score / searchesLen
|
||||
}
|
||||
}
|
||||
return allowSpacesResult
|
||||
}
|
||||
|
||||
if(allowPartialMatch) result = target
|
||||
result._score = score
|
||||
|
||||
var i = 0
|
||||
for (let index of seen_indexes) result._indexes[i++] = index
|
||||
result._indexes.len = i
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// we use this instead of just .normalize('NFD').replace(/[\u0300-\u036f]/g, '') because that screws with japanese characters
|
||||
var remove_accents = (str) => str.replace(/\p{Script=Latin}+/gu, match => match.normalize('NFD')).replace(/[\u0300-\u036f]/g, '')
|
||||
|
||||
var prepareLowerInfo = (str) => {
|
||||
str = remove_accents(str)
|
||||
var strLen = str.length
|
||||
var lower = str.toLowerCase()
|
||||
var lowerCodes = [] // new Array(strLen) sparse array is too slow
|
||||
var bitflags = 0
|
||||
var containsSpace = false // space isn't stored in bitflags because of how searching with a space works
|
||||
|
||||
for(var i = 0; i < strLen; ++i) {
|
||||
var lowerCode = lowerCodes[i] = lower.charCodeAt(i)
|
||||
|
||||
if(lowerCode === 32) {
|
||||
containsSpace = true
|
||||
continue // it's important that we don't set any bitflags for space
|
||||
}
|
||||
|
||||
var bit = lowerCode>=97&&lowerCode<=122 ? lowerCode-97 // alphabet
|
||||
: lowerCode>=48&&lowerCode<=57 ? 26 // numbers
|
||||
// 3 bits available
|
||||
: lowerCode<=127 ? 30 // other ascii
|
||||
: 31 // other utf8
|
||||
bitflags |= 1<<bit
|
||||
}
|
||||
|
||||
return {lowerCodes:lowerCodes, bitflags:bitflags, containsSpace:containsSpace, _lower:lower}
|
||||
}
|
||||
var prepareBeginningIndexes = (target) => {
|
||||
var targetLen = target.length
|
||||
var beginningIndexes = []; var beginningIndexesLen = 0
|
||||
var wasUpper = false
|
||||
var wasAlphanum = false
|
||||
for(var i = 0; i < targetLen; ++i) {
|
||||
var targetCode = target.charCodeAt(i)
|
||||
var isUpper = targetCode>=65&&targetCode<=90
|
||||
var isAlphanum = isUpper || targetCode>=97&&targetCode<=122 || targetCode>=48&&targetCode<=57
|
||||
var isBeginning = isUpper && !wasUpper || !wasAlphanum || !isAlphanum
|
||||
wasUpper = isUpper
|
||||
wasAlphanum = isAlphanum
|
||||
if(isBeginning) beginningIndexes[beginningIndexesLen++] = i
|
||||
}
|
||||
return beginningIndexes
|
||||
}
|
||||
var prepareNextBeginningIndexes = (target) => {
|
||||
target = remove_accents(target)
|
||||
var targetLen = target.length
|
||||
var beginningIndexes = prepareBeginningIndexes(target)
|
||||
var nextBeginningIndexes = [] // new Array(targetLen) sparse array is too slow
|
||||
var lastIsBeginning = beginningIndexes[0]
|
||||
var lastIsBeginningI = 0
|
||||
for(var i = 0; i < targetLen; ++i) {
|
||||
if(lastIsBeginning > i) {
|
||||
nextBeginningIndexes[i] = lastIsBeginning
|
||||
} else {
|
||||
lastIsBeginning = beginningIndexes[++lastIsBeginningI]
|
||||
nextBeginningIndexes[i] = lastIsBeginning===undefined ? targetLen : lastIsBeginning
|
||||
}
|
||||
}
|
||||
return nextBeginningIndexes
|
||||
}
|
||||
|
||||
var preparedCache = new Map()
|
||||
var preparedSearchCache = new Map()
|
||||
|
||||
// the theory behind these being globals is to reduce garbage collection by not making new arrays
|
||||
var matchesSimple = []; var matchesStrict = []
|
||||
var nextBeginningIndexesChanges = [] // allows straw berry to match strawberry well, by modifying the end of a substring to be considered a beginning index for the rest of the search
|
||||
var keysSpacesBestScores = []; var allowPartialMatchScores = []
|
||||
var tmpTargets = []; var tmpResults = []
|
||||
|
||||
// prop = 'key' 2.5ms optimized for this case, seems to be about as fast as direct obj[prop]
|
||||
// prop = 'key1.key2' 10ms
|
||||
// prop = ['key1', 'key2'] 27ms
|
||||
// prop = obj => obj.tags.join() ??ms
|
||||
var getValue = (obj, prop) => {
|
||||
var tmp = obj[prop]; if(tmp !== undefined) return tmp
|
||||
if(typeof prop === 'function') return prop(obj) // this should run first. but that makes string props slower
|
||||
var segs = prop
|
||||
if(!Array.isArray(prop)) segs = prop.split('.')
|
||||
var len = segs.length
|
||||
var i = -1
|
||||
while (obj && (++i < len)) obj = obj[segs[i]]
|
||||
return obj
|
||||
}
|
||||
|
||||
var isPrepared = (x) => { return typeof x === 'object' && typeof x._bitflags === 'number' }
|
||||
var INFINITY = Infinity; var NEGATIVE_INFINITY = -INFINITY
|
||||
var noResults = []; noResults.total = 0
|
||||
var NULL = null
|
||||
|
||||
var noTarget = prepare('')
|
||||
|
||||
// Hacked version of https://github.com/lemire/FastPriorityQueue.js
|
||||
var fastpriorityqueue=r=>{var e=[],o=0,a={},v=r=>{for(var a=0,v=e[a],c=1;c<o;){var s=c+1;a=c,s<o&&e[s]._score<e[c]._score&&(a=s),e[a-1>>1]=e[a],c=1+(a<<1)}for(var f=a-1>>1;a>0&&v._score<e[f]._score;f=(a=f)-1>>1)e[a]=e[f];e[a]=v};return a.add=(r=>{var a=o;e[o++]=r;for(var v=a-1>>1;a>0&&r._score<e[v]._score;v=(a=v)-1>>1)e[a]=e[v];e[a]=r}),a.poll=(r=>{if(0!==o){var a=e[0];return e[0]=e[--o],v(),a}}),a.peek=(r=>{if(0!==o)return e[0]}),a.replaceTop=(r=>{e[0]=r,v()}),a}
|
||||
var q = fastpriorityqueue() // reuse this
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
import qs.config
|
||||
import qs.modules.functions
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
|
||||
PanelWindow {
|
||||
id: backgroundContainer
|
||||
|
||||
required property var modelData
|
||||
property string displayName: modelData.name
|
||||
|
||||
property url wallpaperPath: {
|
||||
const displays = Config.runtime.monitors
|
||||
const fallback = Config.runtime.appearance.background.defaultPath
|
||||
|
||||
if (!displays)
|
||||
return fallback
|
||||
|
||||
const monitor = displays?.[displayName]
|
||||
return monitor?.wallpaper ?? fallback
|
||||
}
|
||||
|
||||
// parallax config
|
||||
property bool parallaxEnabled: Config.runtime.appearance.background.parallax.enabled
|
||||
property real parallaxZoom: Config.runtime.appearance.background.parallax.zoom
|
||||
property int workspaceRange: Config.runtime.bar.modules.workspaces.workspaceIndicators
|
||||
|
||||
// hyprland
|
||||
property int activeWorkspaceId: Hyprland.focusedWorkspace?.id ?? 1
|
||||
|
||||
// wallpaper geometry
|
||||
property real wallpaperWidth: bgImg.implicitWidth
|
||||
property real wallpaperHeight: bgImg.implicitHeight
|
||||
|
||||
property real wallpaperToScreenRatio: {
|
||||
if (wallpaperWidth <= 0 || wallpaperHeight <= 0)
|
||||
return 1
|
||||
return Math.min(
|
||||
wallpaperWidth / width,
|
||||
wallpaperHeight / height
|
||||
)
|
||||
}
|
||||
|
||||
property real effectiveScale: parallaxEnabled ? parallaxZoom : 1
|
||||
|
||||
property real movableXSpace: Math.max(
|
||||
0,
|
||||
((wallpaperWidth / wallpaperToScreenRatio * effectiveScale) - width) / 2
|
||||
)
|
||||
|
||||
// workspace mapping
|
||||
property int lowerWorkspace: Math.floor((activeWorkspaceId - 1) / workspaceRange) * workspaceRange + 1
|
||||
property int upperWorkspace: lowerWorkspace + workspaceRange
|
||||
property int workspaceSpan: Math.max(1, upperWorkspace - lowerWorkspace)
|
||||
|
||||
property real valueX: {
|
||||
if (!parallaxEnabled)
|
||||
return 0.5
|
||||
return (activeWorkspaceId - lowerWorkspace) / workspaceSpan
|
||||
}
|
||||
|
||||
// sidebar globals
|
||||
property bool sidebarLeftOpen: Globals.visiblility.sidebarLeft
|
||||
&& Config.runtime.appearance.background.parallax.enableSidebarLeft
|
||||
|
||||
property bool sidebarRightOpen: Globals.visiblility.sidebarRight
|
||||
&& Config.runtime.appearance.background.parallax.enableSidebarRight
|
||||
|
||||
property real sidebarOffset: {
|
||||
if (sidebarLeftOpen && !sidebarRightOpen)
|
||||
if (Config.runtime.bar.position === "right")
|
||||
return 0.15
|
||||
else return -0.15
|
||||
if (sidebarRightOpen && !sidebarLeftOpen)
|
||||
if (Config.runtime.bar.position === "left")
|
||||
return -0.15
|
||||
else return 0.15
|
||||
return 0
|
||||
}
|
||||
|
||||
property real effectiveValueX: Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
1,
|
||||
valueX + sidebarOffset
|
||||
)
|
||||
)
|
||||
|
||||
// window
|
||||
color: (bgImg.status === Image.Error) ? Appearance.colors.colLayer2 : "transparent"
|
||||
WlrLayershell.namespace: "nucleus:background"
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
WlrLayershell.layer: WlrLayer.Background
|
||||
screen: modelData
|
||||
visible: Config.initialized && Config.runtime.appearance.background.enabled
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
// wallpaper picker
|
||||
Process {
|
||||
id: wallpaperProc
|
||||
|
||||
command: ["bash", "-c", Directories.scriptsPath + "/interface/changebg.sh"]
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const out = text.trim()
|
||||
|
||||
if (out !== "null" && out.length > 0) {
|
||||
const parts = out.split("|")
|
||||
|
||||
if (parts.length === 2) {
|
||||
const monitor = parts[0]
|
||||
const wallpaper = parts[1]
|
||||
|
||||
Config.updateKey(
|
||||
"monitors." + monitor + ".wallpaper",
|
||||
wallpaper
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Quickshell.execDetached([
|
||||
"nucleus", "ipc", "call", "clock", "changePosition"
|
||||
])
|
||||
if (Config.runtime.appearance.colors.autogenerated) {
|
||||
Quickshell.execDetached([
|
||||
"nucleus", "ipc", "call", "global", "regenColors"
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// wallpaper
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
|
||||
StyledImage {
|
||||
id: bgImg
|
||||
|
||||
visible: status === Image.Ready
|
||||
smooth: false
|
||||
cache: false
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
source: wallpaperPath + "?t=" + Date.now()
|
||||
|
||||
width: wallpaperWidth / wallpaperToScreenRatio * effectiveScale
|
||||
height: wallpaperHeight / wallpaperToScreenRatio * effectiveScale
|
||||
|
||||
x: -movableXSpace - (effectiveValueX - 0.5) * 2 * movableXSpace
|
||||
y: 0
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
duration: Metrics.chronoDuration(600)
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
onStatusChanged: {
|
||||
if (status === Image.Ready) {
|
||||
backgroundContainer.wallpaperWidth = implicitWidth
|
||||
backgroundContainer.wallpaperHeight = implicitHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: widgetCanvas
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
// error ui
|
||||
Item {
|
||||
anchors.centerIn: parent
|
||||
visible: bgImg.status === Image.Error
|
||||
|
||||
Rectangle {
|
||||
width: 550
|
||||
height: 400
|
||||
radius: Appearance.rounding.windowRounding
|
||||
color: "transparent"
|
||||
anchors.centerIn: parent
|
||||
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
anchors.margins: Metrics.margin("normal")
|
||||
spacing: Metrics.margin("small")
|
||||
|
||||
MaterialSymbol {
|
||||
text: "wallpaper"
|
||||
font.pixelSize: Metrics.fontSize("wildass")
|
||||
color: Appearance.colors.colOnLayer2
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Wallpaper Missing"
|
||||
font.pixelSize: Metrics.fontSize("hugeass")
|
||||
font.bold: true
|
||||
color: Appearance.colors.colOnLayer2
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Seems like you haven't set a wallpaper yet."
|
||||
font.pixelSize: Metrics.fontSize("small")
|
||||
color: Appearance.colors.colSubtext
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
Item { Layout.fillHeight: true }
|
||||
|
||||
StyledButton {
|
||||
text: "Set wallpaper"
|
||||
icon: "wallpaper"
|
||||
secondary: true
|
||||
radius: Metrics.radius("large")
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
onClicked: wallpaperProc.running = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "background"
|
||||
|
||||
function change() {
|
||||
wallpaperProc.running = true
|
||||
}
|
||||
|
||||
function next() {
|
||||
WallpaperSlideshow.nextWallpaper()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Clock {
|
||||
id: clock
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
import "../../components/morphedPolygons/geometry/offset.js" as Offset
|
||||
import "../../components/morphedPolygons/material-shapes.js" as MaterialShapes // For polygons
|
||||
import "../../components/morphedPolygons/shapes/corner-rounding.js" as CornerRounding
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.modules.components.morphedPolygons
|
||||
import qs.services
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
property bool imageFailed: false
|
||||
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
|
||||
PanelWindow {
|
||||
id: clock
|
||||
|
||||
required property var modelData
|
||||
property int padding: Config.runtime.appearance.background.clock.edgeSpacing
|
||||
property int clockHeight: Config.runtime.appearance.background.clock.isAnalog ? 250 : 160
|
||||
property int clockWidth: Config.runtime.appearance.background.clock.isAnalog ? 250 : 360
|
||||
|
||||
function setRandomPosition() {
|
||||
const x = Math.floor(Math.random() * (width - clockWidth));
|
||||
const y = Math.floor(Math.random() * (height - clockHeight));
|
||||
animX.to = x;
|
||||
animY.to = y;
|
||||
moveAnim.start();
|
||||
Config.updateKey("appearance.background.clock.xPos", x);
|
||||
Config.updateKey("appearance.background.clock.yPos", y);
|
||||
}
|
||||
|
||||
color: "transparent"
|
||||
visible: (Config.runtime.appearance.background.clock.enabled && Config.initialized && !imageFailed)
|
||||
exclusiveZone: 0
|
||||
WlrLayershell.layer: WlrLayer.Bottom
|
||||
screen: modelData
|
||||
|
||||
ParallelAnimation {
|
||||
id: moveAnim
|
||||
|
||||
NumberAnimation {
|
||||
id: animX
|
||||
|
||||
target: rootContentContainer
|
||||
property: "x"
|
||||
duration: Metrics.chronoDuration(400)
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
id: animY
|
||||
|
||||
target: rootContentContainer
|
||||
property: "y"
|
||||
duration: Metrics.chronoDuration(400)
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
bottom: true
|
||||
left: true
|
||||
right: true
|
||||
}
|
||||
|
||||
margins {
|
||||
top: padding
|
||||
bottom: padding
|
||||
left: padding
|
||||
right: padding
|
||||
}
|
||||
|
||||
Item {
|
||||
id: rootContentContainer
|
||||
|
||||
property real releasedX: 0
|
||||
property real releasedY: 0
|
||||
|
||||
height: clockHeight
|
||||
width: clockWidth
|
||||
Component.onCompleted: {
|
||||
Qt.callLater(() => {
|
||||
x = Config.runtime.appearance.background.clock.xPos;
|
||||
y = Config.runtime.appearance.background.clock.yPos;
|
||||
});
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: ma
|
||||
|
||||
anchors.fill: parent
|
||||
drag.target: rootContentContainer
|
||||
drag.axis: Drag.XAndYAxis
|
||||
acceptedButtons: Qt.RightButton
|
||||
onReleased: {
|
||||
if (ma.button === Qt.RightButton)
|
||||
return
|
||||
Config.updateKey("appearance.background.clock.xPos", rootContentContainer.x);
|
||||
Config.updateKey("appearance.background.clock.yPos", rootContentContainer.y);
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: digitalClockContainer
|
||||
|
||||
visible: !Config.runtime.appearance.background.clock.isAnalog
|
||||
|
||||
Column {
|
||||
spacing: Metrics.spacing(-40)
|
||||
|
||||
StyledText {
|
||||
animate: false
|
||||
text: Time.format("hh:mm")
|
||||
font.pixelSize: Metrics.fontSize(Appearance.font.size.wildass * 3)
|
||||
font.family: Metrics.fontFamily("main")
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Metrics.margin(8)
|
||||
animate: false
|
||||
text: Time.format("dddd, dd/MM")
|
||||
font.pixelSize: Metrics.fontSize(32)
|
||||
font.family: Metrics.fontFamily("main")
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
id: analogClockContainer
|
||||
|
||||
property int hours: parseInt(Time.format("hh"))
|
||||
property int minutes: parseInt(Time.format("mm"))
|
||||
property int seconds: parseInt(Time.format("ss"))
|
||||
readonly property real cx: width / 2
|
||||
readonly property real cy: height / 2
|
||||
property var shapes: [MaterialShapes.getCookie7Sided, MaterialShapes.getCookie9Sided, MaterialShapes.getCookie12Sided, MaterialShapes.getPixelCircle, MaterialShapes.getCircle, MaterialShapes.getGhostish]
|
||||
|
||||
anchors.fill: parent
|
||||
visible: Config.runtime.appearance.background.clock.isAnalog
|
||||
width: clock.width / 1.1
|
||||
height: clock.height / 1.1
|
||||
|
||||
// Polygon
|
||||
MorphedPolygon {
|
||||
id: shapeCanvas
|
||||
|
||||
anchors.fill: parent
|
||||
color: Appearance.m3colors.m3secondaryContainer
|
||||
roundedPolygon: analogClockContainer.shapes[Config.runtime.appearance.background.clock.shape]()
|
||||
|
||||
transform: Rotation {
|
||||
origin.x: shapeCanvas.width / 2
|
||||
origin.y: shapeCanvas.height / 2
|
||||
angle: shapeCanvas.rotation
|
||||
}
|
||||
|
||||
NumberAnimation on rotation {
|
||||
from: 0
|
||||
to: 360
|
||||
running: Config.runtime.appearance.animations.enabled && Config.runtime.appearance.background.clock.rotatePolygonBg
|
||||
duration: Config.runtime.appearance.background.clock.rotationDuration * 1000
|
||||
loops: Animation.Infinite
|
||||
}
|
||||
}
|
||||
|
||||
ClockDial {
|
||||
id: dial
|
||||
anchors.fill: parent
|
||||
anchors.margins: parent.width * 0.12
|
||||
color: Appearance.colors.colOnSecondaryContainer
|
||||
z: 0
|
||||
}
|
||||
|
||||
// Hour hand
|
||||
StyledRect {
|
||||
z: 2
|
||||
width: 10
|
||||
height: parent.height * 0.3
|
||||
radius: Metrics.radius("full")
|
||||
color: Qt.darker(Appearance.m3colors.m3secondary, 0.8)
|
||||
x: analogClockContainer.cx - width / 2
|
||||
y: analogClockContainer.cy - height
|
||||
transformOrigin: Item.Bottom
|
||||
rotation: (analogClockContainer.hours % 12 + analogClockContainer.minutes / 60) * 30
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
anchors.centerIn: parent
|
||||
width: 16
|
||||
height: 16
|
||||
radius: width / 2
|
||||
color: Appearance.m3colors.m3secondary
|
||||
z: 99 // Ensures its on top of everthing
|
||||
|
||||
// Inner dot
|
||||
StyledRect {
|
||||
width: parent.width / 2
|
||||
height: parent.height / 2
|
||||
radius: width / 2
|
||||
anchors.centerIn: parent
|
||||
z: 100
|
||||
color: Appearance.m3colors.m3primaryContainer
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Minute hand
|
||||
StyledRect {
|
||||
width: 18
|
||||
height: parent.height * 0.35
|
||||
radius: Metrics.radius("full")
|
||||
color: Appearance.m3colors.m3secondary
|
||||
x: analogClockContainer.cx - width / 2
|
||||
y: analogClockContainer.cy - height
|
||||
transformOrigin: Item.Bottom
|
||||
rotation: analogClockContainer.minutes * 6
|
||||
z: 10 // On top of all hands
|
||||
}
|
||||
|
||||
// Second hand
|
||||
StyledRect {
|
||||
visible: true
|
||||
width: 4
|
||||
height: parent.height * 0.28
|
||||
radius: Metrics.radius("full")
|
||||
color: Appearance.m3colors.m3error
|
||||
x: analogClockContainer.cx - width / 2
|
||||
y: analogClockContainer.cy - height
|
||||
transformOrigin: Item.Bottom
|
||||
rotation: analogClockContainer.seconds * 6
|
||||
z: 2
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: Time.format("hh")
|
||||
anchors.top: parent.top
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.topMargin: Metrics.margin(30)
|
||||
font.pixelSize: Metrics.fontSize(80)
|
||||
font.bold: true
|
||||
opacity: 0.3
|
||||
animate: false
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: Time.format("mm")
|
||||
anchors.top: parent.top
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.topMargin: Metrics.margin(110)
|
||||
font.pixelSize: Metrics.fontSize(80)
|
||||
font.bold: true
|
||||
opacity: 0.3
|
||||
animate: false
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function changePosition() {
|
||||
clock.setRandomPosition();
|
||||
}
|
||||
|
||||
target: "clock"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import QtQuick
|
||||
import qs.modules.components
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property color color: "white"
|
||||
readonly property real cx: width / 2
|
||||
readonly property real cy: height / 2
|
||||
readonly property real radius: Math.min(width, height) / 2
|
||||
opacity: 0.4
|
||||
|
||||
// Hour marks (12 ticks)
|
||||
Repeater {
|
||||
model: 12
|
||||
|
||||
Item {
|
||||
width: root.width
|
||||
height: root.height
|
||||
anchors.centerIn: parent
|
||||
rotation: index * 30
|
||||
transformOrigin: Item.Center
|
||||
|
||||
Rectangle {
|
||||
width: 3 // thickness of tick
|
||||
height: 15 // length of tick
|
||||
color: root.color
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
y: -root.radius * 0.15 / 2
|
||||
radius: width / 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Minute marks (60 ticks)
|
||||
Repeater {
|
||||
model: 60
|
||||
|
||||
Item {
|
||||
width: root.width
|
||||
height: root.height
|
||||
anchors.centerIn: parent
|
||||
rotation: index * 6
|
||||
transformOrigin: Item.Center
|
||||
|
||||
Rectangle {
|
||||
width: index % 5 === 0 ? 3 : 2 // thicker for 5-minute marks
|
||||
height: index % 5 === 0 ? 15 : 8 // longer for 5-minute marks
|
||||
color: root.color
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
y: -root.radius * 0.15 / 2
|
||||
radius: width / 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
163
.config/quickshell/nucleus-shell/modules/interface/bar/Bar.qml
Normal file
163
.config/quickshell/nucleus-shell/modules/interface/bar/Bar.qml
Normal file
@@ -0,0 +1,163 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.config
|
||||
import qs.services
|
||||
import qs.modules.components
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
GothCorners {
|
||||
opacity: ConfigResolver.bar(bar.displayName).gothCorners && !ConfigResolver.bar(bar.displayName).floating && ConfigResolver.bar(bar.displayName).enabled && !ConfigResolver.bar(bar.displayName).merged ? 1 : 0
|
||||
}
|
||||
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
|
||||
PanelWindow {
|
||||
// some exclusiveSpacing so it won't look like its sticking into the window when floating
|
||||
|
||||
id: bar
|
||||
|
||||
required property var modelData
|
||||
property string displayName: modelData.name
|
||||
property int rd: ConfigResolver.bar(displayName).radius * Config.runtime.appearance.rounding.factor // So it won't be modified when factor is 0
|
||||
property int margin: ConfigResolver.bar(displayName).margins
|
||||
property bool floating: ConfigResolver.bar(displayName).floating
|
||||
property bool merged: ConfigResolver.bar(displayName).merged
|
||||
property string pos: ConfigResolver.bar(displayName).position
|
||||
property bool vertical: pos === "left" || pos === "right"
|
||||
// Simple position properties
|
||||
property bool attachedTop: pos === "top"
|
||||
property bool attachedBottom: pos === "bottom"
|
||||
property bool attachedLeft: pos === "left"
|
||||
property bool attachedRight: pos === "right"
|
||||
|
||||
screen: modelData // Show bar on all screens
|
||||
visible: ConfigResolver.bar(displayName).enabled && Config.initialized
|
||||
WlrLayershell.namespace: "nucleus:bar"
|
||||
exclusiveZone: ConfigResolver.bar(displayName).floating ? ConfigResolver.bar(displayName).density + Metrics.margin("tiny") : ConfigResolver.bar(displayName).density
|
||||
implicitHeight: ConfigResolver.bar(displayName).density // density === height. (horizontal orientation)
|
||||
implicitWidth: ConfigResolver.bar(displayName).density // density === width. (vertical orientation)
|
||||
color: "transparent" // Keep panel window's color transparent, so that it can be modified by background rect
|
||||
|
||||
// This is probably a little weird way to set anchors but I think it's the best way. (and it works)
|
||||
anchors {
|
||||
top: ConfigResolver.bar(displayName).position === "top" || ConfigResolver.bar(displayName).position === "left" || ConfigResolver.bar(displayName).position === "right"
|
||||
bottom: ConfigResolver.bar(displayName).position === "bottom" || ConfigResolver.bar(displayName).position === "left" || ConfigResolver.bar(displayName).position === "right"
|
||||
left: ConfigResolver.bar(displayName).position === "left" || ConfigResolver.bar(displayName).position === "top" || ConfigResolver.bar(displayName).position === "bottom"
|
||||
right: ConfigResolver.bar(displayName).position === "right" || ConfigResolver.bar(displayName).position === "top" || ConfigResolver.bar(displayName).position === "bottom"
|
||||
}
|
||||
|
||||
margins {
|
||||
top: {
|
||||
if (floating)
|
||||
return margin;
|
||||
|
||||
if (merged && vertical)
|
||||
return margin;
|
||||
|
||||
return 0;
|
||||
}
|
||||
bottom: {
|
||||
if (floating)
|
||||
return margin;
|
||||
|
||||
if (merged && vertical)
|
||||
return margin;
|
||||
|
||||
return 0;
|
||||
}
|
||||
left: {
|
||||
if (floating)
|
||||
return margin;
|
||||
|
||||
if (merged && !vertical)
|
||||
return margin;
|
||||
|
||||
return 0;
|
||||
}
|
||||
right: {
|
||||
if (floating)
|
||||
return margin;
|
||||
|
||||
if (merged && !vertical)
|
||||
return margin;
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: background
|
||||
color: Appearance.m3colors.m3background
|
||||
anchors.fill: parent
|
||||
topLeftRadius: {
|
||||
if (floating)
|
||||
return rd;
|
||||
|
||||
if (!merged)
|
||||
return 0;
|
||||
|
||||
return attachedBottom || attachedRight ? rd : 0;
|
||||
}
|
||||
topRightRadius: {
|
||||
if (floating)
|
||||
return rd;
|
||||
|
||||
if (!merged)
|
||||
return 0;
|
||||
|
||||
return attachedBottom || attachedLeft ? rd : 0;
|
||||
}
|
||||
bottomLeftRadius: {
|
||||
if (floating)
|
||||
return rd;
|
||||
|
||||
if (!merged)
|
||||
return 0;
|
||||
|
||||
return attachedTop || attachedRight ? rd : 0;
|
||||
}
|
||||
bottomRightRadius: {
|
||||
if (floating)
|
||||
return rd;
|
||||
|
||||
if (!merged)
|
||||
return 0;
|
||||
|
||||
return attachedTop || attachedLeft ? rd : 0;
|
||||
}
|
||||
|
||||
BarContent {
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
Behavior on bottomLeftRadius {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
Behavior on topLeftRadius {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
Behavior on bottomRightRadius {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
Behavior on topRightRadius {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import "content/"
|
||||
import qs.config
|
||||
import qs.services
|
||||
import qs.modules.components
|
||||
|
||||
Item {
|
||||
property string displayName: screen?.name ?? ""
|
||||
property bool isHorizontal: (ConfigResolver.bar(displayName).position === "top" || ConfigResolver.bar(displayName).position === "bottom")
|
||||
|
||||
Row {
|
||||
id: hCenterRow
|
||||
visible: isHorizontal
|
||||
anchors.centerIn: parent
|
||||
spacing: Metrics.spacing(4)
|
||||
|
||||
SystemUsageModule {}
|
||||
MediaPlayerModule {}
|
||||
ActiveWindowModule {}
|
||||
ClockModule {}
|
||||
BatteryIndicatorModule {}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: hLeftRow
|
||||
|
||||
visible: isHorizontal
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Metrics.spacing(4)
|
||||
anchors.leftMargin: ConfigResolver.bar(displayName).density * 0.3
|
||||
|
||||
ToggleModule {
|
||||
icon: "menu"
|
||||
iconSize: Metrics.iconSize(22)
|
||||
iconColor: Appearance.m3colors.m3error
|
||||
toggle: Globals.visiblility.sidebarLeft
|
||||
|
||||
onToggled: function(value) {
|
||||
Globals.visiblility.sidebarLeft = value
|
||||
}
|
||||
}
|
||||
|
||||
WorkspaceModule {}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: hRightRow
|
||||
|
||||
visible: isHorizontal
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Metrics.spacing(4)
|
||||
anchors.rightMargin: ConfigResolver.bar(displayName).density * 0.3
|
||||
|
||||
SystemTray {
|
||||
id: sysTray
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: seperator
|
||||
visible: (sysTray.items.count > 0) && ConfigResolver.bar(displayName).modules.statusIcons.enabled
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
font.pixelSize: Metrics.fontSize("hugeass")
|
||||
text: "·"
|
||||
}
|
||||
|
||||
StatusIconsModule {}
|
||||
|
||||
StyledText {
|
||||
id: seperator2
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
font.pixelSize: Metrics.fontSize("hugeass")
|
||||
text: "·"
|
||||
}
|
||||
|
||||
ToggleModule {
|
||||
icon: "power_settings_new"
|
||||
iconSize: Metrics.iconSize(22)
|
||||
iconColor: Appearance.m3colors.m3error
|
||||
toggle: Globals.visiblility.powermenu
|
||||
|
||||
onToggled: function(value) {
|
||||
Globals.visiblility.powermenu = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Vertical Layout
|
||||
Item {
|
||||
visible: !isHorizontal
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: ConfigResolver.bar(displayName).density * 0.1
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
implicitWidth: vRow.implicitHeight
|
||||
implicitHeight: vRow.implicitWidth
|
||||
|
||||
Row {
|
||||
id: vRow
|
||||
anchors.centerIn: parent
|
||||
spacing: Metrics.spacing(8)
|
||||
rotation: 90
|
||||
|
||||
ToggleModule {
|
||||
icon: "menu"
|
||||
iconSize: Metrics.iconSize(22)
|
||||
iconColor: Appearance.m3colors.m3error
|
||||
toggle: Globals.visiblility.sidebarLeft
|
||||
rotation: 270
|
||||
|
||||
onToggled: function(value) {
|
||||
Globals.visiblility.sidebarLeft = value
|
||||
}
|
||||
}
|
||||
|
||||
SystemUsageModule {}
|
||||
MediaPlayerModule {}
|
||||
|
||||
SystemTray {
|
||||
rotation: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
visible: !isHorizontal
|
||||
anchors.centerIn: parent
|
||||
anchors.verticalCenterOffset: 35
|
||||
implicitWidth: centerRow.implicitHeight
|
||||
implicitHeight: centerRow.implicitWidth
|
||||
|
||||
Row {
|
||||
id: centerRow
|
||||
anchors.centerIn: parent
|
||||
|
||||
WorkspaceModule {
|
||||
rotation: 90
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
visible: !isHorizontal
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: ConfigResolver.bar(displayName).density * 0.1
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
implicitWidth: row.implicitHeight
|
||||
implicitHeight: row.implicitWidth
|
||||
|
||||
Row {
|
||||
id: row
|
||||
anchors.centerIn: parent
|
||||
spacing: Metrics.spacing(6)
|
||||
rotation: 90
|
||||
|
||||
ClockModule {
|
||||
rotation: 270
|
||||
}
|
||||
|
||||
StatusIconsModule {}
|
||||
BatteryIndicatorModule {}
|
||||
|
||||
ToggleModule {
|
||||
icon: "power_settings_new"
|
||||
iconSize: Metrics.iconSize(22)
|
||||
iconColor: Appearance.m3colors.m3error
|
||||
toggle: Globals.visiblility.powermenu
|
||||
rotation: 270
|
||||
|
||||
onToggled: function(value) {
|
||||
Globals.visiblility.powermenu = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
property int opacity: 0
|
||||
|
||||
color: "transparent"
|
||||
visible: Config.initialized
|
||||
WlrLayershell.layer: WlrLayer.Top
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
bottom: true
|
||||
right: true
|
||||
}
|
||||
|
||||
Item {
|
||||
id: container
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
StyledRect {
|
||||
anchors.fill: parent
|
||||
color: Appearance.m3colors.m3background
|
||||
layer.enabled: true
|
||||
opacity: root.opacity
|
||||
|
||||
layer.effect: MultiEffect {
|
||||
maskSource: mask
|
||||
maskEnabled: true
|
||||
maskInverted: true
|
||||
maskThresholdMin: 0.5
|
||||
maskSpreadAtMin: 1
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
NumberAnimation {
|
||||
duration: Metrics.chronoDuration("large")
|
||||
easing.type: Easing.InOutExpo
|
||||
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
id: mask
|
||||
|
||||
anchors.fill: parent
|
||||
layer.enabled: true
|
||||
visible: false
|
||||
|
||||
StyledRect {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: Config.runtime.bar.position === "bottom" ? -15 : 0
|
||||
anchors.bottomMargin: Config.runtime.bar.position === "top" ? -15 : 0
|
||||
anchors.leftMargin: Config.runtime.bar.position === "right" ? -15 : 0
|
||||
anchors.rightMargin: Config.runtime.bar.position === "left" ? -15 : 0
|
||||
radius: Metrics.radius("normal")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
mask: Region {
|
||||
item: container
|
||||
intersection: Intersection.Xor
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.modules.functions
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: container
|
||||
property string displayName: screen?.name ?? ""
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
|
||||
|
||||
property Toplevel activeToplevel: Compositor.isWorkspaceOccupied(Compositor.focusedWorkspaceId)
|
||||
? Compositor.activeToplevel
|
||||
: null
|
||||
|
||||
implicitWidth: row.implicitWidth + 30
|
||||
implicitHeight: ConfigResolver.bar(displayName).modules.height
|
||||
|
||||
function simplifyTitle(title) {
|
||||
if (!title)
|
||||
return ""
|
||||
|
||||
title = title.replace(/[●⬤○◉◌◎]/g, "") // Symbols to remove
|
||||
|
||||
// Normalize separators
|
||||
title = title
|
||||
.replace(/\s*[|—]\s*/g, " - ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim()
|
||||
|
||||
const parts = title.split(" - ").map(p => p.trim()).filter(Boolean)
|
||||
|
||||
if (parts.length === 1)
|
||||
return parts[0]
|
||||
|
||||
// Known app names (extend freely my fellow contributors)
|
||||
const apps = [
|
||||
"Firefox", "Mozilla Firefox",
|
||||
"Chromium", "Google Chrome",
|
||||
"Neovim", "VS Code", "Code",
|
||||
"Kitty", "Alacritty", "Terminal",
|
||||
"Discord", "Spotify", "Steam",
|
||||
"Settings - Nucleus", "Settings"
|
||||
]
|
||||
|
||||
let app = ""
|
||||
for (let i = parts.length - 1; i >= 0; i--) { // loop over
|
||||
for (let a of apps) {
|
||||
if (parts[i].includes(a)) {
|
||||
app = a
|
||||
break
|
||||
}
|
||||
}
|
||||
if (app) break
|
||||
}
|
||||
|
||||
if (!app)
|
||||
app = parts[parts.length - 1]
|
||||
|
||||
const context = parts.find(p => p !== app)
|
||||
|
||||
return context ? `${app} · ${context}` : app
|
||||
}
|
||||
|
||||
|
||||
function formatAppId(appId) { // Random ass function to make it look good
|
||||
if (!appId || appId.length === 0)
|
||||
return "";
|
||||
|
||||
// split on dashes/underscores
|
||||
const parts = appId.split(/[-_]/);
|
||||
// capitalize each segment
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const p = parts[i];
|
||||
parts[i] = p.charAt(0).toUpperCase() + p.slice(1);
|
||||
}
|
||||
return parts.join("-");
|
||||
}
|
||||
|
||||
/* Column {
|
||||
id: col
|
||||
anchors.centerIn: parent
|
||||
|
||||
StyledText {
|
||||
id: workspaceText
|
||||
font.pixelSize: Metrics.fontSize("smallie")
|
||||
text: {
|
||||
if (!activeToplevel)
|
||||
return "Desktop"
|
||||
|
||||
const id = activeToplevel.appId || ""
|
||||
|
||||
return id // Just for aesthetics
|
||||
}
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: titleText
|
||||
text: StringUtils.shortText(simplifyTitle(activeToplevel?.title, 24) || `Workspace ${Hyprland.focusedWorkspaceId}`)
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font.pixelSize: Metrics.fontSize("smalle")
|
||||
}
|
||||
} */
|
||||
|
||||
Rectangle {
|
||||
visible: (ConfigResolver.bar(displayName).position === "top" || ConfigResolver.bar(displayName).position === "bottom")
|
||||
color: Appearance.m3colors.m3paddingContainer
|
||||
anchors.fill: parent
|
||||
height: 34
|
||||
width: row.height + 30
|
||||
radius: ConfigResolver.bar(displayName).modules.radius
|
||||
}
|
||||
|
||||
|
||||
RowLayout {
|
||||
id: row
|
||||
spacing: 12
|
||||
anchors.centerIn: parent
|
||||
|
||||
MaterialSymbol {
|
||||
icon: "desktop_windows"
|
||||
rotation: (ConfigResolver.bar(displayName).position === "left" || ConfigResolver.bar(displayName).position === "right") ? 270 : 0
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: StringUtils.shortText(simplifyTitle(activeToplevel?.title), 24) || `Workspace ${Hyprland.focusedWorkspaceId}`
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font.pixelSize: Appearance.font.size.small
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
|
||||
Item {
|
||||
id: batteryIndicatorModuleContainer
|
||||
|
||||
visible: UPower.batteryPresent
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
// Determine if bar is isVertical
|
||||
property bool isVertical: ConfigResolver.bar(screen?.name ?? "").position === "left" || ConfigResolver.bar(screen?.name ?? "").position === "right"
|
||||
|
||||
implicitWidth: bgRect.implicitWidth
|
||||
implicitHeight: bgRect.implicitHeight
|
||||
|
||||
Rectangle {
|
||||
id: bgRect
|
||||
color: isVertical ? Appearance.m3colors.m3primary : Appearance.m3colors.m3paddingContainer
|
||||
radius: ConfigResolver.bar(screen?.name ?? "").modules.radius * Config.runtime.appearance.rounding.factor // No need to use metrics here...
|
||||
|
||||
implicitWidth: child.implicitWidth + Appearance.margin.large - (isVertical ? 10 : 0)
|
||||
implicitHeight: ConfigResolver.bar(screen?.name ?? "").modules.height
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: child
|
||||
anchors.centerIn: parent
|
||||
spacing: isVertical ? 0 : Metrics.spacing(8)
|
||||
|
||||
// Icon for isVertical bars
|
||||
MaterialSymbol {
|
||||
visible: isVertical
|
||||
icon: UPower.battIcon
|
||||
iconSize: Metrics.iconSize(20)
|
||||
}
|
||||
|
||||
// Battery percentage text
|
||||
StyledText {
|
||||
animate: false
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
rotation: isVertical ? 270 : 0
|
||||
text: (isVertical ? UPower.percentage : UPower.percentage + "%")
|
||||
}
|
||||
|
||||
// Circular progress for horizontal bars
|
||||
CircularProgressBar {
|
||||
visible: !isVertical
|
||||
value: UPower.percentage / 100
|
||||
icon: UPower.battIcon
|
||||
iconSize: Metrics.iconSize(18)
|
||||
Layout.bottomMargin: Metrics.margin(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
|
||||
Item {
|
||||
id: clockContainer
|
||||
|
||||
property string format: isVertical ? "hh\nmm\nAP" : "hh:mm • dd/MM"
|
||||
property bool isVertical: (ConfigResolver.bar(screen?.name ?? "").position === "left" || ConfigResolver.bar(screen?.name ?? "").position === "right")
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
implicitWidth: 37
|
||||
implicitHeight: 30
|
||||
|
||||
AnimatedImage {
|
||||
id: art
|
||||
anchors.fill: parent
|
||||
source: Directories.assetsPath + "/gifs/bongo-cat.gif"
|
||||
cache: false // this is important
|
||||
smooth: true // smooooooth
|
||||
rotation: isVertical ? 270 : 0
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
|
||||
Item {
|
||||
id: clockContainer
|
||||
|
||||
property string format: isVertical ? "hh\nmm\nAP" : "hh:mm • dd/MM"
|
||||
property bool isVertical: (ConfigResolver.bar(screen?.name ?? "").position === "left" || ConfigResolver.bar(screen?.name ?? "").position === "right")
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
implicitWidth: bgRect.implicitWidth
|
||||
implicitHeight: bgRect.implicitHeight
|
||||
|
||||
// Let the layout compute size automatically
|
||||
|
||||
Rectangle {
|
||||
id: bgRect
|
||||
|
||||
color: isVertical ? "transparent" : Appearance.m3colors.m3paddingContainer
|
||||
radius: ConfigResolver.bar(screen?.name ?? "").modules.radius * Config.runtime.appearance.rounding.factor
|
||||
// Padding around the text
|
||||
implicitWidth: isVertical ? textItem.implicitWidth + 40 : textItem.implicitWidth + Metrics.margin("large")
|
||||
implicitHeight: ConfigResolver.bar(screen?.name ?? "").modules.height
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: textItem
|
||||
anchors.centerIn: parent
|
||||
animate: false
|
||||
text: Time.format(clockContainer.format)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Io
|
||||
|
||||
import qs.config
|
||||
import qs.modules.functions
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
|
||||
Item {
|
||||
id: mediaPlayer
|
||||
|
||||
property bool isVertical: (
|
||||
ConfigResolver.bar(screen?.name ?? "").position === "left" ||
|
||||
ConfigResolver.bar(screen?.name ?? "").position === "right"
|
||||
)
|
||||
|
||||
Layout.alignment: Qt.AlignCenter | Qt.AlignVCenter
|
||||
|
||||
implicitWidth: bgRect.implicitWidth
|
||||
implicitHeight: bgRect.implicitHeight
|
||||
|
||||
|
||||
Rectangle {
|
||||
id: bgRect
|
||||
|
||||
color: Appearance.m3colors.m3paddingContainer
|
||||
radius: ConfigResolver.bar(screen?.name ?? "").modules.radius *
|
||||
Config.runtime.appearance.rounding.factor
|
||||
|
||||
implicitWidth: isVertical
|
||||
? row.implicitWidth + Metrics.margin("large") - 10
|
||||
: row.implicitWidth + Metrics.margin("large")
|
||||
|
||||
implicitHeight: ConfigResolver.bar(screen?.name ?? "").modules.height
|
||||
}
|
||||
|
||||
|
||||
Row {
|
||||
id: row
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: Metrics.margin("small")
|
||||
|
||||
|
||||
ClippingRectangle {
|
||||
id: iconButton
|
||||
|
||||
width: 24
|
||||
height: 24
|
||||
radius: ConfigResolver.bar(screen?.name ?? "").modules.radius / 1.2
|
||||
|
||||
color: Appearance.colors.colLayer1Hover
|
||||
opacity: 0.9
|
||||
|
||||
clip: true
|
||||
layer.enabled: true
|
||||
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
|
||||
Image {
|
||||
id: art
|
||||
|
||||
anchors.fill: parent
|
||||
visible: Mpris.artUrl !== ""
|
||||
|
||||
source: Mpris.artUrl
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
smooth: true
|
||||
mipmap: true
|
||||
}
|
||||
|
||||
|
||||
MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
|
||||
visible: Mpris.artUrl === ""
|
||||
icon: "music_note"
|
||||
|
||||
iconSize: 18
|
||||
color: Config.runtime.appearance.theme === "dark"
|
||||
? "#b1a4a4"
|
||||
: "grey"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
|
||||
onClicked: Mpris.playPause()
|
||||
|
||||
onEntered: iconButton.opacity = 1
|
||||
onExited: iconButton.opacity = 0.9
|
||||
}
|
||||
|
||||
|
||||
RotationAnimation on rotation {
|
||||
from: 0
|
||||
to: 360
|
||||
|
||||
duration: Metrics.chronoDuration(4000)
|
||||
loops: Animation.Infinite
|
||||
|
||||
running: Mpris.isPlaying &&
|
||||
Config.runtime.appearance.animations.enabled
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
StyledText {
|
||||
id: textItem
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
text: StringUtils.shortText(Mpris.title, 16)
|
||||
|
||||
visible: !mediaPlayer.isVertical
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
|
||||
Item {
|
||||
id: statusIconsContainer
|
||||
|
||||
property bool isVertical: (ConfigResolver.bar(screen?.name ?? "").position === "left" || ConfigResolver.bar(screen?.name ?? "").position === "right")
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: ConfigResolver.bar(screen?.name ?? "").modules.statusIcons.enabled
|
||||
implicitWidth: bgRect.implicitWidth
|
||||
implicitHeight: bgRect.implicitHeight
|
||||
|
||||
StyledRect {
|
||||
id: bgRect
|
||||
|
||||
color: Globals.visiblility.sidebarRight ? Appearance.m3colors.m3paddingContainer : "transparent"
|
||||
radius: ConfigResolver.bar(screen?.name ?? "").modules.radius * Config.runtime.appearance.rounding.factor
|
||||
implicitWidth: isVertical ? contentRow.implicitWidth + Metrics.margin("large") - 8 : contentRow.implicitWidth + Metrics.margin("large")
|
||||
implicitHeight: ConfigResolver.bar(screen?.name ?? "").modules.height
|
||||
|
||||
RowLayout {
|
||||
id: contentRow
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: isVertical ? Metrics.spacing(8) : Metrics.spacing(16)
|
||||
|
||||
|
||||
MaterialSymbol {
|
||||
id: wifi
|
||||
animate: false
|
||||
visible: ConfigResolver.bar(screen?.name ?? "").modules.statusIcons.networkStatusEnabled
|
||||
rotation: isVertical ? 270 : 0
|
||||
icon: Network.icon
|
||||
iconSize: Metrics.fontSize("huge")
|
||||
}
|
||||
|
||||
MaterialSymbol {
|
||||
id: btIcon
|
||||
animate: false
|
||||
visible: ConfigResolver.bar(screen?.name ?? "").modules.statusIcons.bluetoothStatusEnabled
|
||||
rotation: isVertical ? 270 : 0
|
||||
icon: Bluetooth.icon
|
||||
iconSize: Metrics.fontSize("huge")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
if (Globals.visiblility.sidebarLeft)
|
||||
return
|
||||
Globals.visiblility.sidebarRight = !Globals.visiblility.sidebarRight
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import qs.modules.components
|
||||
import qs.config
|
||||
import qs.services
|
||||
import Quickshell.Services.SystemTray
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
readonly property Repeater items: items
|
||||
property bool horizontalMode: (ConfigResolver.bar(screen?.name ?? "").position === "top" || ConfigResolver.bar(screen?.name ?? "").position === "bottom")
|
||||
clip: true
|
||||
implicitWidth: layout.implicitWidth + Metrics.margin("verylarge")
|
||||
implicitHeight: 34
|
||||
|
||||
Rectangle {
|
||||
visible: (items.count > 0) ? 1 : 0
|
||||
id: padding
|
||||
implicitHeight: padding.height
|
||||
anchors.fill: parent
|
||||
radius: ConfigResolver.bar(screen?.name ?? "").modules.radius
|
||||
color: "transparent"
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
id: layout
|
||||
anchors.centerIn: parent
|
||||
rows: 1
|
||||
columns: items.count
|
||||
rowSpacing: Metrics.spacing(10)
|
||||
columnSpacing: Metrics.spacing(10)
|
||||
|
||||
Repeater {
|
||||
id: items
|
||||
model: SystemTray.items
|
||||
|
||||
delegate: Item {
|
||||
id: trayItemRoot
|
||||
required property SystemTrayItem modelData
|
||||
implicitWidth: 20
|
||||
implicitHeight: 20
|
||||
|
||||
IconImage {
|
||||
visible: trayItemRoot.modelData.icon !== ""
|
||||
source: trayItemRoot.modelData.icon
|
||||
asynchronous: true
|
||||
anchors.fill: parent
|
||||
rotation: root.horizontalMode ? 0 : 270
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: hover
|
||||
}
|
||||
|
||||
QsMenuOpener {
|
||||
id: menuOpener
|
||||
menu: trayItemRoot.modelData.menu
|
||||
}
|
||||
|
||||
StyledPopout {
|
||||
id: popout
|
||||
hoverTarget: hover
|
||||
interactable: true
|
||||
hCenterOnItem: true
|
||||
requiresHover: false
|
||||
|
||||
Component {
|
||||
Item {
|
||||
width: childColumn.implicitWidth
|
||||
height: childColumn.height
|
||||
|
||||
ColumnLayout {
|
||||
id: childColumn
|
||||
spacing: Metrics.spacing(5)
|
||||
|
||||
Repeater {
|
||||
model: menuOpener.children
|
||||
delegate: TrayMenuItem {
|
||||
parentColumn: childColumn
|
||||
Layout.preferredWidth: childColumn.width > 0 ? childColumn.width : implicitWidth
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
hoverEnabled: true
|
||||
|
||||
onClicked: {
|
||||
if (popout.isVisible)
|
||||
popout.hide();
|
||||
else
|
||||
popout.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component TrayMenuItem: Item {
|
||||
id: itemRoot
|
||||
required property QsMenuEntry modelData
|
||||
required property ColumnLayout parentColumn
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitWidth: rowLayout.implicitWidth + 10
|
||||
implicitHeight: !itemRoot.modelData.isSeparator ? rowLayout.implicitHeight + 10 : 1
|
||||
|
||||
MouseArea {
|
||||
id: hover
|
||||
hoverEnabled: itemRoot.modelData.enabled
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
if (!itemRoot.modelData.hasChildren)
|
||||
itemRoot.modelData.triggered();
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: itemBg
|
||||
anchors.fill: parent
|
||||
opacity: itemRoot.modelData.isSeparator ? 0.5 : 1
|
||||
color: itemRoot.modelData.isSeparator ? Appearance.m3colors.m3outline : hover.containsMouse ? Appearance.m3colors.m3surfaceContainer : Appearance.m3colors.m3surface
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: rowLayout
|
||||
visible: !itemRoot.modelData.isSeparator
|
||||
opacity: itemRoot.modelData.isSeparator ? 0.5 : 1
|
||||
spacing: Metrics.spacing(5)
|
||||
anchors {
|
||||
left: itemBg.left
|
||||
leftMargin: Metrics.margin(5)
|
||||
top: itemBg.top
|
||||
topMargin:Metrics.margin(5)
|
||||
}
|
||||
|
||||
IconImage {
|
||||
visible: itemRoot.modelData.icon !== ""
|
||||
source: itemRoot.modelData.icon
|
||||
width: 15
|
||||
height: 15
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: itemRoot.modelData.text
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
}
|
||||
|
||||
MaterialSymbol {
|
||||
visible: itemRoot.modelData.hasChildren
|
||||
icon: "chevron_right"
|
||||
iconSize: Metrics.iconSize(16)
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
|
||||
Item {
|
||||
id: systemUsageContainer
|
||||
property string displayName: screen?.name ?? ""
|
||||
property bool isHorizontal: (ConfigResolver.bar(displayName).position === "top" || ConfigResolver.bar(displayName).position === "bottom")
|
||||
|
||||
visible: ConfigResolver.bar(displayName).modules.systemUsage.enabled && haveWidth
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
implicitWidth: bgRect.implicitWidth
|
||||
implicitHeight: bgRect.implicitHeight
|
||||
|
||||
property bool haveWidth:
|
||||
ConfigResolver.bar(displayName).modules.systemUsage.tempStatsEnabled ||
|
||||
ConfigResolver.bar(displayName).modules.systemUsage.cpuStatsEnabled ||
|
||||
ConfigResolver.bar(displayName).modules.systemUsage.memoryStatsEnabled
|
||||
|
||||
|
||||
// Normalize values so UI always receives correct ranges
|
||||
function normalize(v) {
|
||||
if (v > 1) return v / 100
|
||||
return v
|
||||
}
|
||||
|
||||
function percent(v) {
|
||||
if (v <= 1) return Math.round(v * 100)
|
||||
return Math.round(v)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: bgRect
|
||||
color: Appearance.m3colors.m3paddingContainer
|
||||
radius: ConfigResolver.bar(displayName).modules.radius * Config.runtime.appearance.rounding.factor
|
||||
|
||||
implicitWidth: child.implicitWidth + Metrics.margin("large")
|
||||
implicitHeight: ConfigResolver.bar(displayName).modules.height
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: child
|
||||
anchors.centerIn: parent
|
||||
spacing: Metrics.spacing(4)
|
||||
|
||||
// CPU
|
||||
CircularProgressBar {
|
||||
rotation: !isHorizontal ? 270 : 0
|
||||
icon: "developer_board"
|
||||
visible: ConfigResolver.bar(displayName).modules.systemUsage.cpuStatsEnabled
|
||||
iconSize: Metrics.iconSize(14)
|
||||
value: normalize(SystemDetails.cpuPercent)
|
||||
Layout.bottomMargin: Metrics.margin(2)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: ConfigResolver.bar(displayName).modules.systemUsage.cpuStatsEnabled && isHorizontal
|
||||
animate: false
|
||||
text: percent(SystemDetails.cpuPercent) + "%"
|
||||
}
|
||||
|
||||
// RAM
|
||||
CircularProgressBar {
|
||||
rotation: !isHorizontal ? 270 : 0
|
||||
Layout.leftMargin: Metrics.margin(4)
|
||||
icon: "memory_alt"
|
||||
visible: ConfigResolver.bar(displayName).modules.systemUsage.memoryStatsEnabled
|
||||
iconSize: Metrics.iconSize(14)
|
||||
value: normalize(SystemDetails.ramPercent)
|
||||
Layout.bottomMargin: Metrics.margin(2)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: ConfigResolver.bar(displayName).modules.systemUsage.memoryStatsEnabled && isHorizontal
|
||||
animate: false
|
||||
text: percent(SystemDetails.ramPercent) + "%"
|
||||
}
|
||||
|
||||
// Temperature
|
||||
CircularProgressBar {
|
||||
rotation: !isHorizontal ? 270 : 0
|
||||
visible: ConfigResolver.bar(displayName).modules.systemUsage.tempStatsEnabled
|
||||
Layout.leftMargin: Metrics.margin(4)
|
||||
icon: "device_thermostat"
|
||||
iconSize: Metrics.iconSize(14)
|
||||
value: normalize(SystemDetails.cpuTempPercent)
|
||||
Layout.bottomMargin: Metrics.margin(2)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: ConfigResolver.bar(displayName).modules.systemUsage.tempStatsEnabled && isHorizontal
|
||||
animate: false
|
||||
text: percent(SystemDetails.cpuTempPercent) + "%"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import QtQuick.Layouts
|
||||
|
||||
StyledRect {
|
||||
id: bg
|
||||
|
||||
property string icon
|
||||
property color iconColor: Appearance.syntaxHighlightingTheme
|
||||
property int iconSize
|
||||
property bool toggle
|
||||
property bool transparentBg: false
|
||||
|
||||
signal toggled(bool value)
|
||||
|
||||
color: (ma.containsMouse && !transparentBg)
|
||||
? Appearance.m3colors.m3paddingContainer
|
||||
: "transparent"
|
||||
|
||||
radius: Metrics.radius("childish")
|
||||
|
||||
implicitWidth: textItem.implicitWidth + 12
|
||||
implicitHeight: textItem.implicitHeight + 6
|
||||
|
||||
MaterialSymbol {
|
||||
id: textItem
|
||||
anchors.centerIn: parent
|
||||
anchors.verticalCenterOffset: 0.4
|
||||
anchors.horizontalCenterOffset: 0.499
|
||||
iconSize: bg.iconSize
|
||||
icon: bg.icon
|
||||
color: bg.iconColor
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: ma
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: bg.toggled(!bg.toggle)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.modules.functions
|
||||
import qs.services
|
||||
|
||||
Item {
|
||||
id: workspaceContainer
|
||||
property string displayName: screen?.name ?? ""
|
||||
property int numWorkspaces: ConfigResolver.bar(displayName).modules.workspaces.workspaceIndicators
|
||||
property var workspaceOccupied: []
|
||||
property var occupiedRanges: []
|
||||
|
||||
function japaneseNumber(num) {
|
||||
var kanjiMap = {
|
||||
"0": "零",
|
||||
"1": "一",
|
||||
"2": "二",
|
||||
"3": "三",
|
||||
"4": "四",
|
||||
"5": "五",
|
||||
"6": "六",
|
||||
"7": "七",
|
||||
"8": "八",
|
||||
"9": "九",
|
||||
"10": "十"
|
||||
};
|
||||
return kanjiMap[num] !== undefined ? kanjiMap[num] : "Number out of range";
|
||||
}
|
||||
|
||||
function updateWorkspaceOccupied() {
|
||||
const offset = 1;
|
||||
workspaceOccupied = Array.from({
|
||||
"length": numWorkspaces
|
||||
}, (_, i) => {
|
||||
return Compositor.isWorkspaceOccupied(i + 1);
|
||||
});
|
||||
const ranges = [];
|
||||
let start = -1;
|
||||
for (let i = 0; i < workspaceOccupied.length; i++) {
|
||||
if (workspaceOccupied[i]) {
|
||||
if (start === -1)
|
||||
start = i;
|
||||
|
||||
} else if (start !== -1) {
|
||||
ranges.push({
|
||||
"start": start,
|
||||
"end": i - 1
|
||||
});
|
||||
start = -1;
|
||||
}
|
||||
}
|
||||
if (start !== -1)
|
||||
ranges.push({
|
||||
"start": start,
|
||||
"end": workspaceOccupied.length - 1
|
||||
});
|
||||
|
||||
occupiedRanges = ranges;
|
||||
}
|
||||
|
||||
visible: ConfigResolver.bar(displayName).modules.workspaces.enabled
|
||||
implicitWidth: bg.implicitWidth
|
||||
implicitHeight: ConfigResolver.bar(displayName).modules.height
|
||||
Component.onCompleted: updateWorkspaceOccupied()
|
||||
|
||||
Connections {
|
||||
function onStateChanged() {
|
||||
updateWorkspaceOccupied();
|
||||
}
|
||||
|
||||
target: Compositor
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: bg
|
||||
|
||||
color: Appearance.m3colors.m3paddingContainer
|
||||
radius: ConfigResolver.bar(displayName).modules.radius * Config.runtime.appearance.rounding.factor
|
||||
implicitWidth: workspaceRow.implicitWidth + Metrics.margin("large") - 8
|
||||
implicitHeight: ConfigResolver.bar(displayName).modules.height
|
||||
|
||||
// occupied background highlight
|
||||
Item {
|
||||
id: occupiedStretchLayer
|
||||
|
||||
anchors.centerIn: workspaceRow
|
||||
width: workspaceRow.width
|
||||
height: 26
|
||||
z: 0
|
||||
visible: Compositor.require("hyprland") // Hyprland only
|
||||
|
||||
Repeater {
|
||||
model: occupiedRanges
|
||||
|
||||
Rectangle {
|
||||
height: 26
|
||||
radius: ConfigResolver.bar(displayName).modules.radius * Config.runtime.appearance.rounding.factor
|
||||
color: ColorUtils.mix(Appearance.m3colors.m3tertiary, Appearance.m3colors.m3surfaceContainerLowest)
|
||||
opacity: 0.8
|
||||
x: modelData.start * (26 + workspaceRow.spacing)
|
||||
width: (modelData.end - modelData.start + 1) * 26 + (modelData.end - modelData.start) * workspaceRow.spacing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// workspace highlight
|
||||
Rectangle {
|
||||
id: highlight
|
||||
|
||||
property int offset: Compositor.require("hyprland") ? 1 : 0
|
||||
property int index: Math.max(0, Compositor.focusedWorkspaceId - 1 - offset)
|
||||
property real itemWidth: 26
|
||||
property real spacing: workspaceRow.spacing
|
||||
property int highlightIndex: {
|
||||
if (!Compositor.focusedWorkspaceId)
|
||||
return 0;
|
||||
|
||||
if (Compositor.require("hyprland"))
|
||||
return Compositor.focusedWorkspaceId - 1;
|
||||
// Hyprland starts at 2 internally
|
||||
return Compositor.focusedWorkspaceId - 2; // Niri or default
|
||||
}
|
||||
property real targetX: Math.min(highlightIndex, numWorkspaces - 1) * (itemWidth + spacing) + 7.3
|
||||
property real animatedX1: targetX
|
||||
property real animatedX2: targetX
|
||||
|
||||
x: Math.min(animatedX1, animatedX2)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: Math.abs(animatedX2 - animatedX1) + itemWidth - 1
|
||||
height: 24
|
||||
radius: ConfigResolver.bar(displayName).modules.radius * Config.runtime.appearance.rounding.factor
|
||||
color: Appearance.m3colors.m3tertiary
|
||||
onTargetXChanged: {
|
||||
animatedX1 = targetX;
|
||||
animatedX2 = targetX;
|
||||
}
|
||||
|
||||
Behavior on animatedX1 {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
|
||||
NumberAnimation {
|
||||
duration: Metrics.chronoDuration(400)
|
||||
easing.type: Easing.OutSine
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on animatedX2 {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
|
||||
NumberAnimation {
|
||||
duration: Metrics.chronoDuration(133)
|
||||
easing.type: Easing.OutSine
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: workspaceRow
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: Metrics.spacing(10)
|
||||
|
||||
Repeater {
|
||||
model: numWorkspaces
|
||||
|
||||
Item {
|
||||
property int wsIndex: index + 1
|
||||
property bool occupied: Compositor.isWorkspaceOccupied(wsIndex)
|
||||
property bool focused: wsIndex === Compositor.focusedWorkspaceId
|
||||
|
||||
width: 26
|
||||
height: 26
|
||||
|
||||
// Icon container — only used on Hyprland
|
||||
ClippingRectangle {
|
||||
id: iconContainer
|
||||
|
||||
anchors.centerIn: parent
|
||||
width: 20
|
||||
height: 20
|
||||
color: "transparent"
|
||||
radius: Appearance.rounding.small
|
||||
clip: true
|
||||
|
||||
IconImage {
|
||||
id: appIcon
|
||||
|
||||
anchors.fill: parent
|
||||
visible: Compositor.require("hyprland") && ConfigResolver.bar(displayName).modules.workspaces.showAppIcons && occupied
|
||||
rotation: (ConfigResolver.bar(displayName).position === "left" || ConfigResolver.bar(displayName).position === "right") ? 270 : 0
|
||||
source: {
|
||||
const win = Compositor.focusedWindowForWorkspace(wsIndex);
|
||||
return win ? AppRegistry.iconForClass(win.class) : "";
|
||||
}
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
saturation: (Config.runtime.appearance.tintIcons || (Config.runtime.appearance.colors.matugenScheme === "scheme-monochrome" && Config.runtime.appearance.colors.autogenerated) || Config.runtime.appearance.colors.scheme.toLowerCase() === "monochrome") ? -1.0 : 1.0
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Kanji mode — only if not Hyprland
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
visible: ConfigResolver.bar(displayName).modules.workspaces.showJapaneseNumbers && !ConfigResolver.bar(displayName).modules.workspaces.showAppIcons
|
||||
text: japaneseNumber(index + 1)
|
||||
rotation: (ConfigResolver.bar(displayName).position === "left" || ConfigResolver.bar(displayName).position === "right") ? 270 : 0
|
||||
}
|
||||
|
||||
// Numbers mode — only if not Hyprland
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
visible: !ConfigResolver.bar(displayName).modules.workspaces.showJapaneseNumbers && !ConfigResolver.bar(displayName).modules.workspaces.showAppIcons
|
||||
text: index + 1
|
||||
rotation: (ConfigResolver.bar(displayName).position === "left" || ConfigResolver.bar(displayName).position === "right") ? 270 : 0
|
||||
}
|
||||
|
||||
// Symbols for unoccupied workspaces — only for Hyprland icons
|
||||
MaterialSymbol {
|
||||
property string displayText: Config.runtime.appearance.rounding.factor === 0 ? "crop_square" : "fiber_manual_record"
|
||||
|
||||
anchors.centerIn: parent
|
||||
visible: Compositor.require("hyprland") && ConfigResolver.bar(displayName).modules.workspaces.showAppIcons && !occupied
|
||||
text: displayText
|
||||
rotation: (ConfigResolver.bar(displayName).position === "left" || ConfigResolver.bar(displayName).position === "right") ? 270 : 0
|
||||
font.pixelSize: Metrics.iconSize(10)
|
||||
fill: 1
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: Compositor.changeWorkspace(wsIndex)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,576 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Window
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.config
|
||||
import qs.modules.functions
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
|
||||
FloatingWindow {
|
||||
id: appWin
|
||||
color: Appearance.m3colors.m3background
|
||||
property bool initialChatSelected: false
|
||||
property bool chatsInitialized: false
|
||||
|
||||
function appendMessage(sender, message) {
|
||||
messageModel.append({
|
||||
"sender": sender,
|
||||
"message": message
|
||||
});
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
function updateChatsList(files) {
|
||||
let existing = {
|
||||
};
|
||||
for (let i = 0; i < chatListModel.count; i++) existing[chatListModel.get(i).name] = true
|
||||
for (let file of files) {
|
||||
let name = file.trim();
|
||||
if (!name.length)
|
||||
continue;
|
||||
|
||||
if (name.endsWith(".txt"))
|
||||
name = name.slice(0, -4);
|
||||
|
||||
if (!existing[name])
|
||||
chatListModel.append({
|
||||
"name": name
|
||||
});
|
||||
|
||||
delete existing[name];
|
||||
}
|
||||
// remove chats that no longer exist
|
||||
for (let name in existing) {
|
||||
for (let i = 0; i < chatListModel.count; i++) {
|
||||
if (chatListModel.get(i).name === name) {
|
||||
chatListModel.remove(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// ensure default exists
|
||||
let hasDefault = false;
|
||||
for (let i = 0; i < chatListModel.count; i++) if (chatListModel.get(i).name === "default") {
|
||||
hasDefault = true;
|
||||
}
|
||||
if (!hasDefault) {
|
||||
chatListModel.insert(0, {
|
||||
"name": "default"
|
||||
});
|
||||
FileUtils.createFile(FileUtils.trimFileProtocol(Directories.config) + "/zenith/chats/default.txt");
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
// Always scroll to end after appending
|
||||
chatView.forceLayout();
|
||||
chatView.positionViewAtEnd();
|
||||
}
|
||||
|
||||
function sendMessage() {
|
||||
if (userInput.text === "" || Zenith.loading)
|
||||
return ;
|
||||
|
||||
Zenith.pendingInput = userInput.text;
|
||||
appendMessage("You", userInput.text);
|
||||
userInput.text = "";
|
||||
Zenith.loading = true;
|
||||
Zenith.send();
|
||||
}
|
||||
|
||||
function loadChatHistory(chatName) {
|
||||
messageModel.clear();
|
||||
Zenith.loadChat(chatName);
|
||||
}
|
||||
|
||||
function selectDefaultChat() {
|
||||
let defaultIndex = -1;
|
||||
for (let i = 0; i < chatListModel.count; i++) {
|
||||
if (chatListModel.get(i).name === "default") {
|
||||
defaultIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (defaultIndex !== -1) {
|
||||
chatSelector.currentIndex = defaultIndex;
|
||||
Zenith.currentChat = "default";
|
||||
loadChatHistory("default");
|
||||
} else if (chatListModel.count > 0) {
|
||||
chatSelector.currentIndex = 0;
|
||||
Zenith.currentChat = chatListModel.get(0).name;
|
||||
loadChatHistory(Zenith.currentChat);
|
||||
}
|
||||
}
|
||||
|
||||
visible: Globals.states.intelligenceWindowOpen
|
||||
|
||||
onVisibleChanged: {
|
||||
if (!visible)
|
||||
return ;
|
||||
|
||||
chatsInitialized = false;
|
||||
messageModel.clear();
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function openWindow() {
|
||||
Globals.states.intelligenceWindowOpen = true;
|
||||
}
|
||||
|
||||
function closeWindow() {
|
||||
Globals.states.intelligenceWindowOpen = false;
|
||||
}
|
||||
|
||||
target: "intelligence"
|
||||
}
|
||||
|
||||
ListModel {
|
||||
// { sender: "You" | "AI", message: string }
|
||||
|
||||
id: messageModel
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: chatListModel
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Metrics.spacing(8)
|
||||
anchors.centerIn: parent
|
||||
|
||||
StyledText {
|
||||
visible: !Config.runtime.misc.intelligence.enabled
|
||||
text: "Intelligence is disabled!"
|
||||
Layout.leftMargin: Metrics.margin(24)
|
||||
font.pixelSize: Metrics.fontSize("huge")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: !Config.runtime.misc.intelligence.enabled
|
||||
text: "Go to the settings to enable intelligence"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
anchors.fill: parent
|
||||
color: "transparent"
|
||||
visible: Config.runtime.misc.intelligence.enabled
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Metrics.margin(16)
|
||||
spacing: Metrics.spacing(10)
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(10)
|
||||
|
||||
StyledDropDown {
|
||||
id: chatSelector
|
||||
|
||||
Layout.fillWidth: true
|
||||
model: chatListModel
|
||||
textRole: "name"
|
||||
Layout.preferredHeight: 40
|
||||
onCurrentIndexChanged: {
|
||||
if (currentIndex < 0)
|
||||
return ;
|
||||
|
||||
let name = chatListModel.get(currentIndex).name;
|
||||
if (name === Zenith.currentChat)
|
||||
return ;
|
||||
|
||||
Zenith.currentChat = name;
|
||||
loadChatHistory(name);
|
||||
}
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
icon: "add"
|
||||
Layout.preferredWidth: 40
|
||||
onClicked: {
|
||||
let name = "new-chat-" + chatListModel.count;
|
||||
let path = FileUtils.trimFileProtocol(Directories.config) + "/zenith/chats/" + name + ".txt";
|
||||
FileUtils.createFile(path, function(success) {
|
||||
if (success) {
|
||||
chatListModel.append({
|
||||
"name": name
|
||||
});
|
||||
chatSelector.currentIndex = chatListModel.count - 1;
|
||||
Zenith.currentChat = name;
|
||||
messageModel.clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
icon: "edit"
|
||||
Layout.preferredWidth: 40
|
||||
enabled: chatSelector.currentIndex >= 0
|
||||
onClicked: renameDialog.open()
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
icon: "delete"
|
||||
Layout.preferredWidth: 40
|
||||
enabled: chatSelector.currentIndex >= 0 && chatSelector.currentText !== "default"
|
||||
onClicked: {
|
||||
let name = chatSelector.currentText;
|
||||
let path = FileUtils.trimFileProtocol(Directories.config) + "/zenith/chats/" + name + ".txt";
|
||||
FileUtils.removeFile(path, function(success) {
|
||||
if (success) {
|
||||
chatListModel.remove(chatSelector.currentIndex);
|
||||
selectDefaultChat();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(10)
|
||||
|
||||
StyledDropDown {
|
||||
id: modelSelector
|
||||
|
||||
Layout.fillWidth: true
|
||||
model: ["openai/gpt-4o","openai/gpt-4","openai/gpt-3.5-turbo","openai/gpt-4o-mini","anthropic/claude-3.5-sonnet","anthropic/claude-3-haiku","meta-llama/llama-3.3-70b-instruct:free","deepseek/deepseek-r1-0528:free","qwen/qwen3-coder:free"]
|
||||
currentIndex: 0
|
||||
Layout.preferredHeight: 40
|
||||
onCurrentTextChanged: Zenith.currentModel = currentText
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
icon: "close_fullscreen"
|
||||
Layout.preferredWidth: 40
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["nucleus", "ipc", "call", "intelligence", "closeWindow"]);
|
||||
Globals.visiblility.sidebarLeft = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
radius: Metrics.radius("normal")
|
||||
color: Appearance.m3colors.m3surfaceContainerLow
|
||||
|
||||
ScrollView {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
|
||||
ListView {
|
||||
id: chatView
|
||||
|
||||
model: messageModel
|
||||
spacing: Metrics.spacing(8)
|
||||
anchors.fill: parent
|
||||
anchors.margins: Metrics.margin(12)
|
||||
clip: true
|
||||
|
||||
delegate: Item {
|
||||
property bool isCodeBlock: message.split("\n").length > 2 && message.includes("import ") // simple heuristic
|
||||
|
||||
width: chatView.width
|
||||
height: bubble.implicitHeight + 6
|
||||
Component.onCompleted: {
|
||||
chatView.forceLayout();
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Metrics.spacing(8)
|
||||
|
||||
Item {
|
||||
width: sender === "AI" ? 0 : parent.width * 0.2
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: bubble
|
||||
|
||||
radius: Metrics.radius("normal")
|
||||
color: sender === "You" ? Appearance.m3colors.m3primaryContainer : Appearance.m3colors.m3surfaceContainerHigh
|
||||
implicitWidth: Math.min(textItem.implicitWidth + 20, chatView.width * 0.8)
|
||||
implicitHeight: textItem.implicitHeight
|
||||
anchors.right: sender === "You" ? parent.right : undefined
|
||||
anchors.left: sender === "AI" ? parent.left : undefined
|
||||
anchors.topMargin: Metrics.margin(2)
|
||||
|
||||
TextEdit {
|
||||
id: textItem
|
||||
|
||||
text: StringUtils.markdownToHtml(message)
|
||||
wrapMode: TextEdit.Wrap
|
||||
textFormat: TextEdit.RichText
|
||||
readOnly: true // make it selectable but not editable
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
color: Appearance.syntaxHighlightingTheme
|
||||
padding: Metrics.padding(8)
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: ma
|
||||
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
onClicked: {
|
||||
let p = Qt.createQmlObject('import Quickshell; import Quickshell.Io; Process { command: ["wl-copy", "' + message + '"] }', parent);
|
||||
p.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
width: sender === "You" ? 0 : parent.width * 0.2
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
height: 50
|
||||
radius: Metrics.radius("normal")
|
||||
color: Appearance.m3colors.m3surfaceContainer
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Metrics.margin(6)
|
||||
spacing: Metrics.spacing(10)
|
||||
|
||||
StyledTextField {
|
||||
// Shift+Enter → insert newline
|
||||
// Enter → send message
|
||||
|
||||
id: userInput
|
||||
|
||||
Layout.fillWidth: true
|
||||
placeholderText: "Type your message..."
|
||||
font.pixelSize: Metrics.iconSize(14)
|
||||
padding: Metrics.spacing(8)
|
||||
Keys.onPressed: {
|
||||
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||
if (event.modifiers & Qt.ShiftModifier)
|
||||
insert("\n");
|
||||
else
|
||||
sendMessage();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
text: "Send"
|
||||
enabled: userInput.text.trim().length > 0 && !Zenith.loading
|
||||
opacity: enabled ? 1 : 0.5
|
||||
onClicked: sendMessage()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Dialog {
|
||||
id: renameDialog
|
||||
|
||||
title: "Rename Chat"
|
||||
modal: true
|
||||
visible: false
|
||||
standardButtons: Dialog.NoButton
|
||||
x: (appWin.width - 360) / 2 // center horizontally
|
||||
y: (appWin.height - 160) / 2 // center vertically
|
||||
width: 360
|
||||
height: 200
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Metrics.margin(16)
|
||||
spacing: Metrics.spacing(12)
|
||||
|
||||
StyledText {
|
||||
text: "Enter a new name for the chat"
|
||||
font.pixelSize: Metrics.fontSize(18)
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
StyledTextField {
|
||||
id: renameInput
|
||||
|
||||
Layout.fillWidth: true
|
||||
placeholderText: "New name"
|
||||
filled: false
|
||||
highlight: false
|
||||
text: chatSelector.currentText
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
Layout.preferredHeight: 45
|
||||
padding: Metrics.padding(8)
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(12)
|
||||
Layout.alignment: Qt.AlignRight
|
||||
|
||||
StyledButton {
|
||||
text: "Cancel"
|
||||
Layout.preferredWidth: 80
|
||||
onClicked: renameDialog.close()
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
text: "Rename"
|
||||
Layout.preferredWidth: 100
|
||||
enabled: renameInput.text.trim().length > 0 && renameInput.text !== chatSelector.currentText
|
||||
onClicked: {
|
||||
let oldName = chatSelector.currentText;
|
||||
let newName = renameInput.text.trim();
|
||||
let oldPath = FileUtils.trimFileProtocol(Directories.config) + "/zenith/chats/" + oldName + ".txt";
|
||||
let newPath = FileUtils.trimFileProtocol(Directories.config) + "/zenith/chats/" + newName + ".txt";
|
||||
FileUtils.renameFile(oldPath, newPath, function(success) {
|
||||
if (success) {
|
||||
chatListModel.set(chatSelector.currentIndex, {
|
||||
"name": newName
|
||||
});
|
||||
Zenith.currentChat = newName;
|
||||
renameDialog.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
background: StyledRect {
|
||||
color: Appearance.m3colors.m3surfaceContainer
|
||||
radius: Metrics.radius("normal")
|
||||
border.color: Appearance.colors.colOutline
|
||||
border.width: 1
|
||||
}
|
||||
|
||||
header: StyledRect {
|
||||
color: Appearance.m3colors.m3surfaceContainer
|
||||
radius: Metrics.radius("normal")
|
||||
border.color: Appearance.colors.colOutline
|
||||
border.width: 1
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Thinking…"
|
||||
visible: Zenith.loading
|
||||
color: Appearance.colors.colSubtext
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
bottom: parent.bottom
|
||||
leftMargin: Metrics.margin(22)
|
||||
bottomMargin: Metrics.margin(76)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Connections {
|
||||
// only auto-select once
|
||||
function onChatsListed(text) {
|
||||
let lines = text.split(/\r?\n/);
|
||||
let previousChat = Zenith.currentChat;
|
||||
updateChatsList(lines);
|
||||
// select & load once
|
||||
if (!chatsInitialized) {
|
||||
chatsInitialized = true;
|
||||
let index = -1;
|
||||
for (let i = 0; i < chatListModel.count; i++) {
|
||||
if (chatListModel.get(i).name === previousChat) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (index === -1 && chatListModel.count > 0)
|
||||
index = 0;
|
||||
|
||||
if (index !== -1) {
|
||||
chatSelector.currentIndex = index;
|
||||
Zenith.currentChat = chatListModel.get(index).name;
|
||||
loadChatHistory(Zenith.currentChat);
|
||||
}
|
||||
return ;
|
||||
}
|
||||
// AFTER init: only react if current chat vanished
|
||||
let stillExists = false;
|
||||
for (let i = 0; i < chatListModel.count; i++) {
|
||||
if (chatListModel.get(i).name === Zenith.currentChat) {
|
||||
stillExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!stillExists && chatListModel.count > 0) {
|
||||
chatSelector.currentIndex = 0;
|
||||
Zenith.currentChat = chatListModel.get(0).name;
|
||||
loadChatHistory(Zenith.currentChat);
|
||||
}
|
||||
}
|
||||
|
||||
function onAiReply(text) {
|
||||
appendMessage("AI", text.slice(5));
|
||||
Zenith.loading = false;
|
||||
}
|
||||
|
||||
function onChatLoaded(text) {
|
||||
let lines = text.split(/\r?\n/);
|
||||
let batch = [];
|
||||
for (let l of lines) {
|
||||
let line = l.trim();
|
||||
if (!line.length)
|
||||
continue;
|
||||
|
||||
let u = line.match(/^\[\d{4}-.*\] User: (.*)$/);
|
||||
let a = line.match(/^\[\d{4}-.*\] AI: (.*)$/);
|
||||
if (u)
|
||||
batch.push({
|
||||
"sender": "You",
|
||||
"message": u[1]
|
||||
});
|
||||
else if (a)
|
||||
batch.push({
|
||||
"sender": "AI",
|
||||
"message": a[1]
|
||||
});
|
||||
else if (batch.length)
|
||||
batch[batch.length - 1].message += "\n" + line;
|
||||
}
|
||||
messageModel.clear();
|
||||
for (let m of batch) messageModel.append(m)
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
target: Zenith
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Controls
|
||||
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.modules.functions
|
||||
|
||||
StyledRect {
|
||||
id: root
|
||||
property bool hovered: false
|
||||
property bool selected: false
|
||||
|
||||
required property int parentWidth
|
||||
|
||||
width: parentWidth
|
||||
height: 50
|
||||
color: {
|
||||
if (selected || hovered)
|
||||
return Appearance.m3colors.m3surfaceContainerHigh
|
||||
else
|
||||
return Appearance.m3colors.m3surface
|
||||
}
|
||||
radius: Metrics.radius(15)
|
||||
|
||||
Behavior on color {
|
||||
PropertyAnimation {
|
||||
duration: Metrics.chronoDuration(200)
|
||||
easing.type: Easing.InSine
|
||||
}
|
||||
}
|
||||
|
||||
ClippingWrapperRectangle {
|
||||
id: entryIcon
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Metrics.margin(10)
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: (parent.height / 2) - (size / 2)
|
||||
|
||||
property int size: 25
|
||||
height: size
|
||||
width: size
|
||||
radius: Metrics.radius(1000)
|
||||
|
||||
color: "transparent"
|
||||
|
||||
child: Image {
|
||||
source: Quickshell.iconPath(modelData.icon, "application-x-executable")
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect { // Tint if needed, ngl this looks fucking cool when you use monochrome
|
||||
saturation: (Config.runtime.appearance.tintIcons || (Config.runtime.appearance.colors.matugenScheme === "scheme-monochrome" && Config.runtime.appearance.colors.autogenerated) || Config.runtime.appearance.colors.scheme.toLowerCase() === "monochrome") ? -1.0 : 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.left: entryIcon.right
|
||||
anchors.leftMargin: Metrics.margin(10)
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: (parent.height / 2) - (height / 2)
|
||||
|
||||
height: 40
|
||||
spacing: Metrics.spacing(-5)
|
||||
|
||||
StyledText {
|
||||
font.weight: 400
|
||||
text: modelData.name
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
color: {
|
||||
if (root.hovered || root.selected)
|
||||
return Appearance.m3colors.m3onSurface
|
||||
else
|
||||
return Appearance.colors.colOutline
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
PropertyAnimation {
|
||||
duration: Metrics.chronoDuration(200)
|
||||
easing.type: Easing.InSine
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
font.weight: 400
|
||||
text: StringUtils.shortText(modelData.comment, 65) // Limit maximum chars to 65
|
||||
font.pixelSize: Metrics.fontSize(12)
|
||||
color: {
|
||||
if (root.hovered || root.selected)
|
||||
return Qt.alpha(Appearance.m3colors.m3onSurface, 0.7)
|
||||
else
|
||||
return Qt.alpha(Appearance.colors.colOutline, 0.7)
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
PropertyAnimation {
|
||||
duration: Metrics.chronoDuration(200)
|
||||
easing.type: Easing.InSine
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onEntered: root.hovered = true
|
||||
onExited: root.hovered = false
|
||||
onClicked: {
|
||||
modelData.execute()
|
||||
IPCLoader.toggleLauncher()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Wayland
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Controls
|
||||
|
||||
import qs.modules.components
|
||||
import qs.modules.functions
|
||||
import qs.config
|
||||
import qs.services
|
||||
|
||||
PanelWindow {
|
||||
id: launcherWindow
|
||||
|
||||
readonly property bool launcherOpen: Globals.visiblility.launcher
|
||||
|
||||
visible: launcherOpen
|
||||
focusable: true
|
||||
aboveWindows: true // btw I never knew this was a property (read docs)
|
||||
color: "transparent"
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
bottom: true
|
||||
left: true
|
||||
right: true
|
||||
}
|
||||
|
||||
exclusionMode: ExclusionMode.Ignore // why this? idk but it works atleast
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
||||
|
||||
ScrollView {
|
||||
id: maskId
|
||||
|
||||
implicitHeight: DisplayMetrics.scaledHeight(0.623)
|
||||
implicitWidth: DisplayMetrics.scaledWidth(0.3)
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: (parent.width / 2) - (implicitWidth / 2)
|
||||
anchors.topMargin: (parent.height / 2) - (implicitHeight / 2)
|
||||
|
||||
clip: true
|
||||
focus: true
|
||||
|
||||
Rectangle {
|
||||
id: launcher
|
||||
property string currentSearch: ""
|
||||
property int entryIndex: 0
|
||||
property list<DesktopEntry> appList: Apps.list
|
||||
|
||||
Connections {
|
||||
target: launcherWindow
|
||||
function onLauncherOpenChanged() {
|
||||
if (!launcherWindow.launcherOpen) {
|
||||
launcher.currentSearch = ""
|
||||
launcher.entryIndex = 0
|
||||
launcher.appList = Apps.list
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
color: Appearance.m3colors.m3surface
|
||||
radius: Metrics.radius(21)
|
||||
|
||||
StyledRect {
|
||||
id: searchBox
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: Metrics.margin(10)
|
||||
|
||||
color: Appearance.m3colors.m3surfaceContainerLow
|
||||
width: parent.width - 20
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: (parent.width / 2) - (width / 2)
|
||||
height: 45
|
||||
radius: Metrics.radius(15)
|
||||
z: 2
|
||||
|
||||
focus: true
|
||||
|
||||
Keys.onDownPressed: launcher.entryIndex += 1
|
||||
Keys.onUpPressed: {
|
||||
if (launcher.entryIndex != 0)
|
||||
launcher.entryIndex -= 1
|
||||
}
|
||||
Keys.onEscapePressed: Globals.visiblility.launcher = false
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) {
|
||||
launcher.appList[launcher.entryIndex].execute()
|
||||
Globals.visiblility.launcher = false
|
||||
} else if (event.key === Qt.Key_Backspace) {
|
||||
launcher.currentSearch = launcher.currentSearch.slice(0, -1)
|
||||
} else if (" abcdefghijklmnopqrstuvwxyz1234567890`~!@#$%^&*()-_=+[{]}\\|;:'\",<.>/?".includes(event.text.toLowerCase())) {
|
||||
launcher.currentSearch += event.text
|
||||
}
|
||||
|
||||
launcher.appList = Apps.fuzzyQuery(launcher.currentSearch)
|
||||
launcher.entryIndex = 0
|
||||
}
|
||||
|
||||
MaterialSymbol {
|
||||
id: iconText
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Metrics.margin(10)
|
||||
icon: "search"
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
font.weight: 600
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: (parent.height / 2) - ((font.pixelSize + 5) / 2)
|
||||
opacity: 0.8
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: placeHolderText
|
||||
anchors.left: iconText.right
|
||||
anchors.leftMargin: Metrics.margin(10)
|
||||
color: (launcher.currentSearch != "") ? Appearance.m3colors.m3onSurface : Appearance.colors.colOutline
|
||||
text: (launcher.currentSearch != "") ? launcher.currentSearch : "Start typing to search ..."
|
||||
font.pixelSize: Metrics.fontSize(13)
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: (parent.height / 2) - ((font.pixelSize + 5) / 2)
|
||||
animate: false
|
||||
opacity: 0.8
|
||||
}
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
anchors.top: searchBox.bottom
|
||||
anchors.topMargin: Metrics.margin(10)
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: (parent.width / 2) - (width / 2)
|
||||
width: parent.width - 20
|
||||
height: parent.height - searchBox.height - 20
|
||||
|
||||
ListView {
|
||||
id: appList
|
||||
anchors.fill: parent
|
||||
spacing: Metrics.spacing(10)
|
||||
anchors.bottomMargin: Metrics.margin(4)
|
||||
|
||||
model: launcher.appList
|
||||
currentIndex: launcher.entryIndex
|
||||
|
||||
delegate: AppItem {
|
||||
required property int index
|
||||
required property DesktopEntry modelData
|
||||
selected: index === launcher.entryIndex
|
||||
parentWidth: appList.width
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function toggle() {
|
||||
Globals.visiblility.launcher = !Globals.visiblility.launcher;
|
||||
}
|
||||
|
||||
target: "launcher"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.config
|
||||
import qs.modules.functions
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
|
||||
/*
|
||||
|
||||
This LauncherContent has been depricated.
|
||||
And yet not used. (4/3/26)
|
||||
|
||||
*/
|
||||
|
||||
Item {
|
||||
id: content
|
||||
|
||||
property int selectedIndex: -1
|
||||
property string searchQuery: ""
|
||||
property var calcVars: ({})
|
||||
|
||||
property alias listView: listView
|
||||
property alias filteredModel: filteredModel
|
||||
|
||||
function launchCurrent() {
|
||||
launchApp(listView.currentIndex)
|
||||
}
|
||||
|
||||
function webSearchUrl(query) {
|
||||
const engine = (Config.runtime.launcher.webSearchEngine || "").toLowerCase()
|
||||
if (engine.startsWith("http"))
|
||||
return engine.replace("%s", encodeURIComponent(query))
|
||||
|
||||
const engines = {
|
||||
"google": "https://www.google.com/search?q=%s",
|
||||
"duckduckgo": "https://duckduckgo.com/?q=%s",
|
||||
"brave": "https://search.brave.com/search?q=%s",
|
||||
"bing": "https://www.bing.com/search?q=%s",
|
||||
"startpage": "https://www.startpage.com/search?q=%s"
|
||||
}
|
||||
const template = engines[engine] || engines["duckduckgo"]
|
||||
return template.replace("%s", encodeURIComponent(query))
|
||||
}
|
||||
|
||||
function moveSelection(delta) {
|
||||
if (filteredModel.count === 0) return
|
||||
|
||||
selectedIndex = Math.max(0, Math.min(selectedIndex + delta, filteredModel.count - 1))
|
||||
listView.currentIndex = selectedIndex
|
||||
listView.positionViewAtIndex(selectedIndex, ListView.Contain)
|
||||
}
|
||||
|
||||
function fuzzyMatch(text, pattern) {
|
||||
text = text.toLowerCase()
|
||||
pattern = pattern.toLowerCase()
|
||||
let ti = 0, pi = 0
|
||||
while (ti < text.length && pi < pattern.length) {
|
||||
if (text[ti] === pattern[pi]) pi++
|
||||
ti++
|
||||
}
|
||||
return pi === pattern.length
|
||||
}
|
||||
|
||||
function evalExpression(expr) {
|
||||
try {
|
||||
const fn = new Function("vars", `
|
||||
with (vars) { with (Math) { return (${expr}); } }
|
||||
`)
|
||||
const res = fn(calcVars)
|
||||
if (res === undefined || Number.isNaN(res)) return null
|
||||
return res
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function updateFilter() {
|
||||
filteredModel.clear()
|
||||
const query = searchQuery.toLowerCase().trim()
|
||||
|
||||
const calcVal = evalExpression(query)
|
||||
if (calcVal !== null && query !== "") {
|
||||
filteredModel.append({
|
||||
name: String(calcVal),
|
||||
displayName: String(calcVal),
|
||||
comment: "Calculation",
|
||||
icon: "",
|
||||
exec: "",
|
||||
isCalc: true,
|
||||
isWeb: false
|
||||
})
|
||||
}
|
||||
|
||||
const sourceApps = AppRegistry.apps
|
||||
|
||||
if (query === "") {
|
||||
for (let app of sourceApps) {
|
||||
filteredModel.append({
|
||||
name: app.name,
|
||||
displayName: app.name,
|
||||
comment: app.comment,
|
||||
icon: AppRegistry.iconForDesktopIcon(app.icon),
|
||||
exec: app.exec,
|
||||
isCalc: false,
|
||||
isWeb: false
|
||||
})
|
||||
}
|
||||
selectedIndex = filteredModel.count > 0 ? 0 : -1
|
||||
listView.currentIndex = selectedIndex
|
||||
return
|
||||
}
|
||||
|
||||
let exactMatches = []
|
||||
let startsWithMatches = []
|
||||
let containsMatches = []
|
||||
let fuzzyMatches = []
|
||||
|
||||
for (let app of sourceApps) {
|
||||
const name = app.name ? app.name.toLowerCase() : ""
|
||||
const comment = app.comment ? app.comment.toLowerCase() : ""
|
||||
|
||||
if (name === query) exactMatches.push(app)
|
||||
else if (name.startsWith(query)) startsWithMatches.push(app)
|
||||
else if (name.includes(query) || comment.includes(query)) containsMatches.push(app)
|
||||
else if (Config.runtime.launcher.fuzzySearchEnabled && fuzzyMatch(name, query)) fuzzyMatches.push(app)
|
||||
}
|
||||
|
||||
const sortedResults = [
|
||||
...exactMatches,
|
||||
...startsWithMatches,
|
||||
...containsMatches,
|
||||
...fuzzyMatches
|
||||
]
|
||||
|
||||
for (let app of sortedResults) {
|
||||
filteredModel.append({
|
||||
name: app.name,
|
||||
displayName: app.name,
|
||||
comment: app.comment,
|
||||
icon: AppRegistry.iconForDesktopIcon(app.icon),
|
||||
exec: app.exec,
|
||||
isCalc: false,
|
||||
isWeb: false
|
||||
})
|
||||
}
|
||||
|
||||
if (filteredModel.count === 0 && query !== "") {
|
||||
filteredModel.append({
|
||||
name: query,
|
||||
displayName: "Search the web for \"" + query + "\"",
|
||||
comment: "Web search",
|
||||
icon: "public",
|
||||
exec: webSearchUrl(query),
|
||||
isCalc: false,
|
||||
isWeb: true
|
||||
})
|
||||
}
|
||||
|
||||
selectedIndex = filteredModel.count > 0 ? 0 : -1
|
||||
listView.currentIndex = selectedIndex
|
||||
listView.positionViewAtBeginning()
|
||||
}
|
||||
|
||||
function launchApp(idx) {
|
||||
if (idx < 0 || idx >= filteredModel.count) return
|
||||
|
||||
const app = filteredModel.get(idx)
|
||||
if (app.isCalc) return
|
||||
if (app.isWeb)
|
||||
Quickshell.execDetached(["xdg-open", app.exec])
|
||||
else
|
||||
Quickshell.execDetached(["bash", "-c", app.exec + " &"])
|
||||
|
||||
closeLauncher()
|
||||
}
|
||||
|
||||
function closeLauncher() {
|
||||
Globals.visiblility.launcher = false
|
||||
}
|
||||
|
||||
function resetSearch() {
|
||||
searchQuery = ""
|
||||
updateFilter()
|
||||
selectedIndex = -1
|
||||
listView.currentIndex = -1
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: AppRegistry
|
||||
function onReady() {
|
||||
updateFilter()
|
||||
}
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
opacity: Globals.visiblility.launcher ? 1 : 0
|
||||
anchors.margins: Metrics.margin(10)
|
||||
|
||||
ListModel { id: filteredModel }
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Metrics.margin(16)
|
||||
spacing: Metrics.spacing(12)
|
||||
|
||||
ScrollView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
|
||||
ListView {
|
||||
id: listView
|
||||
model: filteredModel
|
||||
spacing: Metrics.spacing(8)
|
||||
clip: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
highlightRangeMode: ListView.StrictlyEnforceRange
|
||||
preferredHighlightBegin: 0
|
||||
preferredHighlightEnd: height
|
||||
highlightMoveDuration: 120
|
||||
currentIndex: selectedIndex
|
||||
|
||||
delegate: Rectangle {
|
||||
property bool isSelected: listView.currentIndex === index
|
||||
|
||||
width: listView.width
|
||||
height: 60
|
||||
radius: Appearance.rounding.normal
|
||||
color: isSelected ? Appearance.m3colors.m3surfaceContainerHighest : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Metrics.margin(10)
|
||||
spacing: Metrics.spacing(12)
|
||||
|
||||
Item {
|
||||
width: 32
|
||||
height: 32
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
visible: !model.isCalc && !model.isWeb
|
||||
smooth: true
|
||||
mipmap: true
|
||||
antialiasing: true
|
||||
fillMode: Image.PreserveAspectFit
|
||||
sourceSize.width: 128
|
||||
sourceSize.height: 128
|
||||
source: model.icon
|
||||
}
|
||||
|
||||
MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
visible: model.isCalc
|
||||
icon: "calculate"
|
||||
iconSize: Metrics.iconSize(28)
|
||||
color: Appearance.m3colors.m3onSurfaceVariant
|
||||
}
|
||||
|
||||
MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
visible: model.isWeb
|
||||
icon: "public"
|
||||
iconSize: Metrics.iconSize(28)
|
||||
color: Appearance.m3colors.m3onSurfaceVariant
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: listView.width - 120
|
||||
spacing: Metrics.spacing(4)
|
||||
|
||||
Text {
|
||||
text: model.displayName
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
font.bold: true
|
||||
elide: Text.ElideRight
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
}
|
||||
|
||||
Text {
|
||||
text: model.comment
|
||||
font.pixelSize: Metrics.fontSize(11)
|
||||
elide: Text.ElideRight
|
||||
color: Appearance.m3colors.m3onSurfaceVariant
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: launchApp(index)
|
||||
onEntered: listView.currentIndex = index
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
NumberAnimation {
|
||||
duration: Metrics.chronoDuration(400)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.animation.curves.standard
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.Pam
|
||||
|
||||
// I just copied the default example and modified it. lol
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
signal unlocked()
|
||||
signal failed()
|
||||
|
||||
// These properties are in the context and not individual lock surfaces
|
||||
// so all surfaces can share the same state.
|
||||
property string currentText: ""
|
||||
property bool unlockInProgress: false
|
||||
property bool showFailure: false
|
||||
|
||||
// Clear the failure text once the user starts typing.
|
||||
onCurrentTextChanged: showFailure = false;
|
||||
|
||||
function tryUnlock() {
|
||||
if (currentText === "") return;
|
||||
|
||||
root.unlockInProgress = true;
|
||||
pam.start();
|
||||
}
|
||||
|
||||
PamContext {
|
||||
id: pam
|
||||
|
||||
// Its best to have a custom pam config for quickshell, as the system one
|
||||
// might not be what your interface expects, and break in some way.
|
||||
// This particular example only supports passwords.
|
||||
configDirectory: "pam"
|
||||
config: "password.conf"
|
||||
|
||||
// pam_unix will ask for a response for the password prompt
|
||||
onPamMessage: {
|
||||
if (this.responseRequired) {
|
||||
this.respond(root.currentText);
|
||||
}
|
||||
}
|
||||
|
||||
// pam_unix won't send any important messages so all we need is the completion status.
|
||||
onCompleted: result => {
|
||||
if (result == PamResult.Success) {
|
||||
root.unlocked();
|
||||
} else {
|
||||
root.currentText = "";
|
||||
root.showFailure = true;
|
||||
}
|
||||
|
||||
root.unlockInProgress = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
|
||||
Scope {
|
||||
// This stores all the information shared between the lock surfaces on each screen.
|
||||
LockContext {
|
||||
id: lockContext
|
||||
|
||||
onUnlocked: {
|
||||
// Unlock the screen before exiting, or the compositor will display a
|
||||
// fallback lock you can't interact with.
|
||||
lock.locked = false;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
WlSessionLock {
|
||||
id: lock
|
||||
|
||||
// Lock the session immediately when quickshell starts.
|
||||
locked: false
|
||||
|
||||
WlSessionLockSurface {
|
||||
LockSurface {
|
||||
anchors.fill: parent
|
||||
context: lockContext
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "lockscreen"
|
||||
function lock() {
|
||||
lock.locked = true;
|
||||
}
|
||||
function unlock() {
|
||||
lock.locked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
import "../../components/morphedPolygons/geometry/offset.js" as Offset
|
||||
import "../../components/morphedPolygons/material-shapes.js" as MaterialShapes // For polygons
|
||||
import "../../components/morphedPolygons/shapes/corner-rounding.js" as CornerRounding
|
||||
import QtQuick
|
||||
import QtQuick.Controls.Fusion
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Wayland
|
||||
import qs.config
|
||||
import qs.modules.functions
|
||||
import qs.modules.interface.background
|
||||
import qs.modules.components
|
||||
import qs.modules.components.morphedPolygons
|
||||
import qs.services
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
required property LockContext context
|
||||
|
||||
color: "transparent"
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
source: Config.runtime.appearance.background.path
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Metrics.spacing(20)
|
||||
|
||||
anchors {
|
||||
top: parent.top
|
||||
right: parent.right
|
||||
topMargin: Metrics.spacing(20)
|
||||
rightMargin: Metrics.spacing(30)
|
||||
}
|
||||
|
||||
MaterialSymbol {
|
||||
id: themeIcon
|
||||
|
||||
fill: 1
|
||||
icon: Config.runtime.appearance.theme === "light" ? "light_mode" : "dark_mode"
|
||||
iconSize: Metrics.fontSize("hugeass")
|
||||
}
|
||||
|
||||
MaterialSymbol {
|
||||
id: wifi
|
||||
|
||||
icon: Network.icon
|
||||
iconSize: Metrics.fontSize("hugeass")
|
||||
}
|
||||
|
||||
MaterialSymbol {
|
||||
id: btIcon
|
||||
|
||||
icon: Bluetooth.icon
|
||||
iconSize: Metrics.fontSize("hugeass")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: keyboardLayoutIcon
|
||||
|
||||
text: SystemDetails.keyboardLayout
|
||||
font.pixelSize: Metrics.fontSize(Appearance.font.size.huge - 4)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
|
||||
anchors {
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
top: parent.top
|
||||
topMargin: Metrics.margin(150)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: clock
|
||||
|
||||
visible: !Config.runtime.appearance.background.clock.isAnalog
|
||||
Layout.alignment: Qt.AlignBottom
|
||||
animate: false
|
||||
renderType: Text.NativeRendering
|
||||
font.pixelSize: Metrics.fontSize(180)
|
||||
text: Time.format("hh:mm")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: date
|
||||
|
||||
visible: !Config.runtime.appearance.background.clock.isAnalog
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
animate: false
|
||||
renderType: Text.NativeRendering
|
||||
font.pixelSize: Metrics.fontSize(50)
|
||||
text: Time.format("dddd, dd/MM")
|
||||
}
|
||||
|
||||
Item {
|
||||
id: analogClockContainer
|
||||
|
||||
property int hours: parseInt(Time.format("hh"))
|
||||
property int minutes: parseInt(Time.format("mm"))
|
||||
property int seconds: parseInt(Time.format("ss"))
|
||||
readonly property real cx: width / 2
|
||||
readonly property real cy: height / 2
|
||||
property var shapes: [MaterialShapes.getCookie7Sided, MaterialShapes.getCookie9Sided, MaterialShapes.getCookie12Sided, MaterialShapes.getPixelCircle, MaterialShapes.getCircle, MaterialShapes.getGhostish]
|
||||
|
||||
visible: Config.runtime.appearance.background.clock.isAnalog
|
||||
width: 350
|
||||
height: 350
|
||||
|
||||
// Polygon
|
||||
MorphedPolygon {
|
||||
id: shapeCanvas
|
||||
|
||||
anchors.fill: parent
|
||||
color: Appearance.m3colors.m3secondaryContainer
|
||||
roundedPolygon: analogClockContainer.shapes[Config.runtime.appearance.background.clock.shape]()
|
||||
}
|
||||
|
||||
ClockDial {
|
||||
anchors.fill: parent
|
||||
anchors.margins: parent.width * 0.12
|
||||
color: Appearance.colors.colOnSecondaryContainer
|
||||
z: 0
|
||||
}
|
||||
|
||||
// Hour hand
|
||||
StyledRect {
|
||||
z: 2
|
||||
width: 10
|
||||
height: parent.height * 0.3
|
||||
radius: Metrics.radius("full")
|
||||
color: Qt.darker(Appearance.m3colors.m3secondary, 0.8)
|
||||
x: analogClockContainer.cx - width / 2
|
||||
y: analogClockContainer.cy - height
|
||||
transformOrigin: Item.Bottom
|
||||
rotation: (analogClockContainer.hours % 12 + analogClockContainer.minutes / 60) * 30
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
anchors.centerIn: parent
|
||||
width: 16
|
||||
height: 16
|
||||
radius: width / 2
|
||||
color: Appearance.m3colors.m3secondary
|
||||
z: 99 // Ensures its on top of everthing
|
||||
|
||||
// Inner dot
|
||||
StyledRect {
|
||||
width: parent.width / 2
|
||||
height: parent.height / 2
|
||||
radius: width / 2
|
||||
anchors.centerIn: parent
|
||||
z: 100
|
||||
color: Appearance.m3colors.m3primaryContainer
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Minute hand
|
||||
StyledRect {
|
||||
width: 14
|
||||
height: parent.height * 0.35
|
||||
radius: Metrics.radius("full")
|
||||
color: Appearance.m3colors.m3secondary
|
||||
x: analogClockContainer.cx - width / 2
|
||||
y: analogClockContainer.cy - height
|
||||
transformOrigin: Item.Bottom
|
||||
rotation: analogClockContainer.minutes * 6
|
||||
z: 10 // On top of all hands
|
||||
}
|
||||
|
||||
// Second hand
|
||||
StyledRect {
|
||||
visible: true
|
||||
width: 4
|
||||
height: parent.height * 0.28
|
||||
radius: Metrics.radius("full")
|
||||
color: Appearance.m3colors.m3error
|
||||
x: analogClockContainer.cx - width / 2
|
||||
y: analogClockContainer.cy - height
|
||||
transformOrigin: Item.Bottom
|
||||
rotation: analogClockContainer.seconds * 6
|
||||
z: 2
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: Time.format("hh")
|
||||
anchors.top: parent.top
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.topMargin: Metrics.margin(60)
|
||||
font.pixelSize: Metrics.fontSize(100)
|
||||
font.bold: true
|
||||
opacity: 0.3
|
||||
animate: false
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: Time.format("mm")
|
||||
anchors.top: parent.top
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.topMargin: Metrics.margin(150)
|
||||
font.pixelSize: Metrics.fontSize(100)
|
||||
font.bold: true
|
||||
opacity: 0.3
|
||||
animate: false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
// Commenting this will make the password entry visible on all monitors.
|
||||
visible: Window.active
|
||||
|
||||
anchors {
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
bottom: parent.bottom
|
||||
bottomMargin: Metrics.margin(20)
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
StyledTextField {
|
||||
id: passwordBox
|
||||
|
||||
implicitWidth: 300
|
||||
padding: Metrics.padding(10)
|
||||
placeholder: root.context.showFailure ? "Incorrect Password" : "Enter Password"
|
||||
focus: true
|
||||
enabled: !root.context.unlockInProgress
|
||||
echoMode: TextInput.Password
|
||||
inputMethodHints: Qt.ImhSensitiveData
|
||||
// Update the text in the context when the text in the box changes.
|
||||
onTextChanged: root.context.currentText = this.text
|
||||
// Try to unlock when enter is pressed.
|
||||
onAccepted: root.context.tryUnlock()
|
||||
|
||||
// Update the text in the box to match the text in the context.
|
||||
// This makes sure multiple monitors have the same text.
|
||||
Connections {
|
||||
function onCurrentTextChanged() {
|
||||
passwordBox.text = root.context.currentText;
|
||||
}
|
||||
|
||||
target: root.context
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
icon: "chevron_right"
|
||||
padding: Metrics.padding(10)
|
||||
radius: Metrics.radius("unsharpenmore")
|
||||
// don't steal focus from the text box
|
||||
focusPolicy: Qt.NoFocus
|
||||
enabled: !root.context.unlockInProgress && root.context.currentText !== ""
|
||||
onClicked: root.context.tryUnlock()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
auth required pam_unix.so
|
||||
@@ -0,0 +1,144 @@
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Wayland
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property bool startAnim: false
|
||||
property string title: "No Title"
|
||||
property string body: "No content"
|
||||
property var rawNotif: null
|
||||
property bool tracked: false
|
||||
property string image: ""
|
||||
property var buttons: [
|
||||
{ label: "Okay!", onClick: () => console.log("Okay") }
|
||||
]
|
||||
|
||||
opacity: tracked ? 1 : (startAnim ? 1 : 0)
|
||||
Behavior on opacity {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
NumberAnimation {
|
||||
duration: Metrics.chronoDuration("small")
|
||||
easing.type: Easing.InOutExpo
|
||||
}
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
radius: Metrics.radius("large")
|
||||
|
||||
property bool hovered: mouseHandler.containsMouse
|
||||
property bool clicked: mouseHandler.containsPress
|
||||
color: hovered ? (clicked ? Appearance.m3colors.m3surfaceContainerHigh : Appearance.m3colors.m3surfaceContainerLow) : Appearance.m3colors.m3surface
|
||||
Behavior on color {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
ColorAnimation {
|
||||
duration: Metrics.chronoDuration("small")
|
||||
easing.type: Easing.InOutExpo
|
||||
}
|
||||
}
|
||||
implicitHeight: Math.max(content.implicitHeight + 30, 80)
|
||||
|
||||
RowLayout {
|
||||
id: content
|
||||
anchors.fill: parent
|
||||
anchors.margins: Metrics.margin(10)
|
||||
spacing: Metrics.spacing(10)
|
||||
|
||||
ClippingRectangle {
|
||||
width: 50
|
||||
height: 50
|
||||
radius: Metrics.radius("large")
|
||||
clip: true
|
||||
color: root.image === "" ? Appearance.m3colors.m3surfaceContainer : "transparent"
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: root.image
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
smooth: true
|
||||
}
|
||||
MaterialSymbol {
|
||||
icon: "chat"
|
||||
color: Appearance.m3colors.m3onSurfaceVariant
|
||||
anchors.centerIn: parent
|
||||
visible: root.image === ""
|
||||
iconSize: Metrics.iconSize(22)
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
StyledText {
|
||||
text: root.title
|
||||
font.bold: true
|
||||
font.pixelSize: Metrics.fontSize(18)
|
||||
wrapMode: Text.Wrap
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.body.length > 123 ? root.body.substr(0, 120) + "..." : root.body
|
||||
visible: root.body.length > 0
|
||||
font.pixelSize: Metrics.fontSize(12)
|
||||
color: Appearance.m3colors.m3onSurfaceVariant
|
||||
wrapMode: Text.Wrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
visible: root.buttons.length > 1
|
||||
Layout.preferredHeight: 40
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(10)
|
||||
|
||||
Repeater {
|
||||
model: buttons
|
||||
|
||||
StyledButton {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 30
|
||||
implicitWidth: 0
|
||||
text: modelData.label
|
||||
base_bg: index !== 0
|
||||
? Appearance.m3colors.m3secondaryContainer
|
||||
: Appearance.m3colors.m3primary
|
||||
|
||||
base_fg: index !== 0
|
||||
? Appearance.m3colors.m3onSecondaryContainer
|
||||
: Appearance.m3colors.m3onPrimary
|
||||
onClicked: modelData.onClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
MouseArea {
|
||||
id: mouseHandler
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
visible: root.buttons.length === 0 || root.buttons.length === 1
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (root.buttons.length === 1 && root.buttons[0].onClick) {
|
||||
root.buttons[0].onClick()
|
||||
root.rawNotif?.notification.dismiss()
|
||||
} else if (root.buttons.length === 0) {
|
||||
console.log("[Notification] Dismissed a notification with no action.")
|
||||
root.rawNotif.notification.tracked = false
|
||||
root.rawNotif.popup = false
|
||||
root.rawNotif?.notification.dismiss()
|
||||
} else {
|
||||
console.log("[Notification] Dismissed a notification with multiple actions.")
|
||||
root.rawNotif?.notification.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
Component.onCompleted: {
|
||||
startAnim = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Services.Notifications
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
property int innerSpacing: Metrics.spacing(10)
|
||||
|
||||
PanelWindow {
|
||||
id: window
|
||||
|
||||
implicitWidth: 520
|
||||
visible: true
|
||||
color: "transparent"
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.exclusionMode: ExclusionMode.Normal
|
||||
WlrLayershell.namespace: "nucleus:notification"
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: Config.runtime.notifications.position.endsWith("left")
|
||||
bottom: true
|
||||
right: Config.runtime.notifications.position.endsWith("right")
|
||||
}
|
||||
|
||||
Item {
|
||||
id: notificationList
|
||||
|
||||
anchors.leftMargin: 0
|
||||
anchors.topMargin: Metrics.margin(10)
|
||||
anchors.rightMargin: 0
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
Rectangle {
|
||||
id: bgRectangle
|
||||
|
||||
layer.enabled: true
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Metrics.margin(20)
|
||||
anchors.rightMargin: Metrics.margin(20)
|
||||
anchors.right: parent.right
|
||||
height: window.mask.height > 0 ? window.mask.height + 40 : 0
|
||||
color: Appearance.m3colors.m3background
|
||||
radius: Metrics.radius("large")
|
||||
|
||||
layer.effect: MultiEffect {
|
||||
shadowEnabled: true
|
||||
shadowOpacity: 1
|
||||
shadowColor: Appearance.m3colors.m3shadow
|
||||
shadowBlur: 1
|
||||
shadowScale: 1
|
||||
}
|
||||
|
||||
Behavior on height {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
NumberAnimation {
|
||||
duration: Metrics.chronoDuration("small")
|
||||
easing.type: Easing.InOutExpo
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
id: notificationColumn
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
Repeater {
|
||||
id: rep
|
||||
|
||||
model: (!Config.runtime.notifications.doNotDisturb && Config.runtime.notifications.enabled) ? NotifServer.popups : []
|
||||
|
||||
NotificationChild {
|
||||
id: child
|
||||
|
||||
width: notificationColumn.width - 80
|
||||
anchors.horizontalCenter: notificationColumn.horizontalCenter
|
||||
y: {
|
||||
var pos = 0;
|
||||
for (let i = 0; i < index; i++) {
|
||||
var prev = rep.itemAt(i);
|
||||
if (prev)
|
||||
pos += prev.height + root.innerSpacing;
|
||||
|
||||
}
|
||||
return pos + 20;
|
||||
}
|
||||
Component.onCompleted: {
|
||||
if (!modelData.shown)
|
||||
modelData.shown = true;
|
||||
|
||||
}
|
||||
title: modelData.summary
|
||||
body: modelData.body
|
||||
image: modelData.image || modelData.appIcon
|
||||
rawNotif: modelData
|
||||
tracked: modelData.shown
|
||||
buttons: modelData.actions.map((action) => {
|
||||
return ({
|
||||
"label": action.text,
|
||||
"onClick": () => {
|
||||
return action.invoke();
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
Behavior on y {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
NumberAnimation {
|
||||
duration: Metrics.chronoDuration("normal")
|
||||
easing.type: Easing.InOutExpo
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
mask: Region {
|
||||
width: window.width
|
||||
height: {
|
||||
var total = 0;
|
||||
for (let i = 0; i < rep.count; i++) {
|
||||
var child = rep.itemAt(i);
|
||||
if (child)
|
||||
total += child.height + (i < rep.count - 1 ? root.innerSpacing : 0);
|
||||
|
||||
}
|
||||
return total;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Wayland
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
Connections {
|
||||
target: Brightness
|
||||
|
||||
function onBrightnessChanged() {
|
||||
root.shouldShowOsd = true;
|
||||
hideTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
property var monitor: Brightness.monitors.length > 0 ? Brightness.monitors[0] : null
|
||||
|
||||
property bool shouldShowOsd: false
|
||||
|
||||
Timer {
|
||||
id: hideTimer
|
||||
interval: 3000
|
||||
onTriggered: root.shouldShowOsd = false
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
active: root.shouldShowOsd
|
||||
|
||||
PanelWindow {
|
||||
visible: Config.runtime.overlays.brightnessOverlayEnabled && Config.runtime.overlays.enabled
|
||||
WlrLayershell.namespace: "nucleus:brightnessOsd"
|
||||
exclusiveZone: 0
|
||||
anchors.top: Config.runtime.overlays.brightnessOverlayPosition.startsWith("top")
|
||||
anchors.bottom: Config.runtime.overlays.brightnessOverlayPosition.startsWith("bottom")
|
||||
anchors.right: Config.runtime.overlays.brightnessOverlayPosition.endsWith("right")
|
||||
anchors.left: Config.runtime.overlays.brightnessOverlayPosition.endsWith("left")
|
||||
margins {
|
||||
top: Metrics.margin(10)
|
||||
bottom: Metrics.margin(10)
|
||||
left: Metrics.margin(10)
|
||||
right: Metrics.margin(10)
|
||||
}
|
||||
|
||||
implicitWidth: 460
|
||||
implicitHeight: 105
|
||||
color: "transparent"
|
||||
mask: Region {}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Appearance.rounding.childish
|
||||
color: Appearance.m3colors.m3background
|
||||
|
||||
RowLayout {
|
||||
spacing: Metrics.spacing(10)
|
||||
anchors {
|
||||
fill: parent
|
||||
leftMargin: Metrics.margin(15)
|
||||
rightMargin: Metrics.margin(25)
|
||||
}
|
||||
|
||||
MaterialSymbol {
|
||||
property real brightnessLevel: Math.floor(Brightness.getMonitorForScreen(Hyprland.focusedMonitor)?.multipliedBrightness*100)
|
||||
icon: {
|
||||
if (brightnessLevel > 66) return "brightness_high"
|
||||
else if (brightnessLevel > 33) return "brightness_medium"
|
||||
else return "brightness_low"
|
||||
}
|
||||
iconSize: Metrics.iconSize(30)
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
implicitHeight: 40
|
||||
spacing: Metrics.spacing(5)
|
||||
|
||||
StyledText {
|
||||
animate: false
|
||||
text: "Brightness - " + Math.round(monitor.brightness * 100) + '%'
|
||||
font.pixelSize: Metrics.fontSize(18)
|
||||
}
|
||||
|
||||
StyledSlider {
|
||||
implicitHeight: 35
|
||||
from: 0
|
||||
to: 100
|
||||
value: Math.round(monitor.brightness * 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
VolumeOverlay{}
|
||||
BrightnessOverlay{}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Services.Pipewire
|
||||
import Quickshell.Widgets
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
PwObjectTracker {
|
||||
objects: [ Pipewire.defaultAudioSink ]
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Pipewire.defaultAudioSink?.audio ?? null
|
||||
|
||||
function onVolumeChanged() {
|
||||
root.shouldShowOsd = true;
|
||||
hideTimer.restart();
|
||||
}
|
||||
|
||||
function onMutedChanged() {
|
||||
root.shouldShowOsd = true;
|
||||
hideTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
property bool shouldShowOsd: false
|
||||
|
||||
Timer {
|
||||
id: hideTimer
|
||||
interval: 3000
|
||||
onTriggered: root.shouldShowOsd = false
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
active: root.shouldShowOsd
|
||||
|
||||
PanelWindow {
|
||||
visible: Config.runtime.overlays.volumeOverlayEnabled && Config.runtime.overlays.enabled
|
||||
WlrLayershell.namespace: "nucleus:brightnessOsd"
|
||||
exclusiveZone: 0
|
||||
anchors.top: Config.runtime.overlays.volumeOverlayPosition.startsWith("top")
|
||||
anchors.bottom: Config.runtime.overlays.volumeOverlayPosition.startsWith("bottom")
|
||||
anchors.right: Config.runtime.overlays.volumeOverlayPosition.endsWith("right")
|
||||
anchors.left: Config.runtime.overlays.volumeOverlayPosition.endsWith("left")
|
||||
margins {
|
||||
top: Metrics.margin(10)
|
||||
bottom: Metrics.margin(10)
|
||||
left: Metrics.margin(10)
|
||||
right: Metrics.margin(10)
|
||||
}
|
||||
implicitWidth: 460
|
||||
implicitHeight: 105
|
||||
color: "transparent"
|
||||
|
||||
mask: Region {}
|
||||
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Appearance.rounding.childish
|
||||
color: Appearance.m3colors.m3background
|
||||
|
||||
RowLayout {
|
||||
spacing: Metrics.spacing(10)
|
||||
anchors {
|
||||
fill: parent
|
||||
leftMargin: Metrics.margin(15)
|
||||
rightMargin: Metrics.margin(25)
|
||||
}
|
||||
|
||||
MaterialSymbol {
|
||||
property real volume: Pipewire.defaultAudioSink?.audio.muted ? 0 : Pipewire.defaultAudioSink?.audio.volume * 100
|
||||
icon: volume > 50 ? "volume_up" : volume > 0 ? "volume_down" : 'volume_off'
|
||||
iconSize: Metrics.iconSize(34);
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
spacing: Metrics.spacing(2)
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
|
||||
animate: false
|
||||
text: (Pipewire.defaultAudioSink?.description ?? "Unknown") + " - " +
|
||||
(Pipewire.defaultAudioSink?.audio.muted ? 'Muted' : Math.floor(Pipewire.defaultAudioSink?.audio.volume * 100) + '%')
|
||||
font.pixelSize: Metrics.fontSize(18)
|
||||
}
|
||||
|
||||
StyledSlider {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 35
|
||||
value: (Pipewire.defaultAudioSink?.audio.muted ? 0 : Pipewire.defaultAudioSink?.audio.volume) * 100
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Effects
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
|
||||
import qs.config
|
||||
import qs.services
|
||||
import qs.modules.components
|
||||
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
property bool active: false
|
||||
property var window: null
|
||||
|
||||
Connections {
|
||||
target: Polkit
|
||||
function onIsActiveChanged() {
|
||||
if (Polkit.isActive) {
|
||||
root.active = true;
|
||||
} else if (root.active && window) {
|
||||
window.closeWithAnimation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
active: root.active
|
||||
component: Prompt {
|
||||
id: window
|
||||
|
||||
Component.onCompleted: root.window = window
|
||||
Component.onDestruction: root.window = null
|
||||
|
||||
onFadeOutFinished: root.active = false
|
||||
|
||||
Item {
|
||||
id: promptContainer
|
||||
property bool showPassword: false
|
||||
property bool authenticating: false
|
||||
|
||||
anchors.centerIn: parent
|
||||
width: promptBg.width
|
||||
height: promptBg.height
|
||||
|
||||
Item {
|
||||
Component.onCompleted: {
|
||||
parent.layer.enabled = true;
|
||||
parent.layer.effect = effectComponent;
|
||||
}
|
||||
|
||||
Component {
|
||||
id: effectComponent
|
||||
MultiEffect {
|
||||
shadowEnabled: true
|
||||
shadowOpacity: 1
|
||||
shadowColor: Appearance.colors.m3shadow
|
||||
shadowBlur: 1
|
||||
shadowScale: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: promptBg
|
||||
width: promptLayout.width + 40
|
||||
height: promptLayout.height + 40
|
||||
color: Appearance.m3colors.m3surface
|
||||
radius: Metrics.radius(20)
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Metrics.chronoDuration("small")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: promptLayout
|
||||
spacing: Metrics.spacing(10)
|
||||
anchors {
|
||||
left: promptBg.left
|
||||
leftMargin: Metrics.margin(20)
|
||||
top: promptBg.top
|
||||
topMargin: Metrics.margin(20)
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Metrics.spacing(5)
|
||||
MaterialSymbol {
|
||||
icon: "security"
|
||||
color: Appearance.m3colors.m3primary
|
||||
font.pixelSize: Metrics.fontSize(22)
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
StyledText {
|
||||
text: "Authentication required"
|
||||
font.family: "Outfit SemiBold"
|
||||
font.pixelSize: Metrics.fontSize(20)
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
StyledText {
|
||||
text: Polkit.flow.message
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Metrics.spacing(5)
|
||||
StyledTextField {
|
||||
id: textfield
|
||||
Layout.fillWidth: true
|
||||
leftPadding: undefined
|
||||
padding: Metrics.padding(10)
|
||||
filled: false
|
||||
enabled: !promptContainer.authenticating
|
||||
placeholder: Polkit.flow.inputPrompt.substring(0, Polkit.flow.inputPrompt.length - 2)
|
||||
echoMode: promptContainer.showPassword ? TextInput.Normal : TextInput.Password
|
||||
inputMethodHints: Qt.ImhSensitiveData
|
||||
focus: true
|
||||
Keys.onReturnPressed: okButton.clicked()
|
||||
}
|
||||
StyledButton {
|
||||
Layout.fillHeight: true
|
||||
width: height
|
||||
radius: Metrics.radius(10)
|
||||
topLeftRadius: Metrics.radius(5)
|
||||
bottomLeftRadius: Metrics.radius(5)
|
||||
enabled: !promptContainer.authenticating
|
||||
checkable: true
|
||||
checked: promptContainer.showPassword
|
||||
icon: promptContainer.showPassword ? 'visibility' : 'visibility_off'
|
||||
onToggled: promptContainer.showPassword = !promptContainer.showPassword
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
RowLayout {
|
||||
RowLayout {
|
||||
visible: Polkit.flow.failed && !Polkit.flow.isSuccessful && !promptContainer.authenticating
|
||||
MaterialSymbol {
|
||||
icon: "warning"
|
||||
color: Appearance.m3colors.m3error
|
||||
font.pixelSize: Metrics.fontSize(15)
|
||||
}
|
||||
StyledText {
|
||||
text: "Failed to authenticate, incorrect password."
|
||||
color: Appearance.m3colors.m3error
|
||||
font.pixelSize: Metrics.fontSize(15)
|
||||
}
|
||||
}
|
||||
LoadingIcon {
|
||||
visible: promptContainer.authenticating
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
}
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
StyledButton {
|
||||
radius: Metrics.radius(10)
|
||||
topRightRadius: Metrics.radius(5)
|
||||
bottomRightRadius: Metrics.radius(5)
|
||||
secondary: true
|
||||
text: "Cancel"
|
||||
// enabled: !promptContainer.authenticating (Allows to cancel if stuck in loop)
|
||||
onClicked: Polkit.flow.cancelAuthenticationRequest()
|
||||
}
|
||||
StyledButton {
|
||||
id: okButton
|
||||
radius: Metrics.radius(10)
|
||||
topLeftRadius: Metrics.radius(5)
|
||||
bottomLeftRadius: Metrics.radius(5)
|
||||
text: promptContainer.authenticating ? "Authenticating..." : "OK"
|
||||
enabled: !promptContainer.authenticating
|
||||
onClicked: {
|
||||
promptContainer.authenticating = true;
|
||||
Polkit.flow.submit(textfield.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Polkit.flow
|
||||
function onIsCompletedChanged() {
|
||||
if (Polkit.flow.isCompleted) {
|
||||
promptContainer.authenticating = false;
|
||||
}
|
||||
}
|
||||
function onFailedChanged() {
|
||||
if (Polkit.flow.failed) {
|
||||
promptContainer.authenticating = false;
|
||||
}
|
||||
}
|
||||
function onIsCancelledChanged() {
|
||||
if (Polkit.flow.isCancelled) {
|
||||
promptContainer.authenticating = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Effects
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
|
||||
import qs.config
|
||||
import qs.services
|
||||
import qs.modules.components
|
||||
|
||||
PanelWindow {
|
||||
id: window
|
||||
property bool isClosing: false
|
||||
default property alias content: contentContainer.data
|
||||
signal fadeOutFinished()
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
||||
color: "transparent"
|
||||
WlrLayershell.namespace: "nucleus:prompt"
|
||||
|
||||
function closeWithAnimation() {
|
||||
if (isClosing) return
|
||||
isClosing = true
|
||||
fadeOutAnim.start()
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
Keys.onPressed: {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
window.closeWithAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
ScreencopyView {
|
||||
id: screencopy
|
||||
visible: hasContent
|
||||
captureSource: window.screen
|
||||
anchors.fill: parent
|
||||
opacity: 0
|
||||
scale: 1
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
blurEnabled: true
|
||||
blur: 1
|
||||
blurMax: 32
|
||||
brightness: -0.05
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
autoPaddingEnabled: false
|
||||
blurEnabled: true
|
||||
blur: 1
|
||||
blurMax: 32
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
id: fadeInAnim
|
||||
target: screencopy
|
||||
property: "opacity"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: Metrics.chronoDuration("normal")
|
||||
easing.type: Appearance.animation.easing
|
||||
running: screencopy.visible && !window.isClosing
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
id: scaleInAnim
|
||||
running: screencopy.visible && !window.isClosing
|
||||
NumberAnimation {
|
||||
target: contentContainer
|
||||
property: "scale"
|
||||
from: 0.9
|
||||
to: 1
|
||||
duration: Metrics.chronoDuration("normal")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
ColorAnimation {
|
||||
target: window
|
||||
property: "color"
|
||||
from: "transparent"
|
||||
to: Appearance.m3colors.m3surface
|
||||
duration: Metrics.chronoDuration("normal")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
NumberAnimation {
|
||||
target: contentContainer
|
||||
property: "opacity"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: Metrics.chronoDuration("normal")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
id: fadeOutAnim
|
||||
NumberAnimation {
|
||||
target: screencopy
|
||||
property: "opacity"
|
||||
to: 0
|
||||
duration: Metrics.chronoDuration("normal")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
ColorAnimation {
|
||||
target: window
|
||||
property: "color"
|
||||
to: "transparent"
|
||||
duration: Metrics.chronoDuration("normal")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
NumberAnimation {
|
||||
target: contentContainer
|
||||
property: "opacity"
|
||||
to: 0
|
||||
duration: Metrics.chronoDuration("normal")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
NumberAnimation {
|
||||
target: contentContainer
|
||||
property: "scale"
|
||||
to: 0.9
|
||||
duration: Metrics.chronoDuration("normal")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
onFinished: {
|
||||
window.visible = false
|
||||
window.fadeOutFinished()
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentContainer
|
||||
anchors.fill: parent
|
||||
opacity: 0
|
||||
scale: 0.9
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Pipewire
|
||||
import Quickshell.Wayland
|
||||
import qs.config
|
||||
import qs.modules.functions
|
||||
import qs.services
|
||||
import qs.modules.interface.lockscreen
|
||||
import qs.modules.components
|
||||
|
||||
PanelWindow {
|
||||
id: powermenu
|
||||
|
||||
WlrLayershell.keyboardFocus: Compositor.require("hyprland") && Globals.visiblility.powermenu
|
||||
|
||||
function togglepowermenu() {
|
||||
Globals.visiblility.powermenu = !Globals.visiblility.powermenu; // Simple toggle logic kept in a function as it might have more things to it later on.
|
||||
}
|
||||
|
||||
WlrLayershell.namespace: "nucleus:powermenu"
|
||||
WlrLayershell.layer: WlrLayer.Top
|
||||
visible: Config.initialized && Globals.visiblility.powermenu
|
||||
color: "transparent"
|
||||
exclusiveZone: 0
|
||||
implicitWidth: DisplayMetrics.scaledWidth(0.25)
|
||||
implicitHeight: DisplayMetrics.scaledWidth(0.168)
|
||||
|
||||
HyprlandFocusGrab {
|
||||
id: grab
|
||||
|
||||
active: Compositor.require("hyprland")
|
||||
windows: [powermenu]
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: container
|
||||
|
||||
color: Appearance.m3colors.m3background
|
||||
radius: Metrics.radius("verylarge")
|
||||
implicitWidth: powermenu.implicitWidth
|
||||
anchors.fill: parent
|
||||
|
||||
FocusScope {
|
||||
focus: true
|
||||
anchors.fill: parent
|
||||
Keys.onPressed: {
|
||||
if (event.key === Qt.Key_Escape)
|
||||
Globals.visiblility.powermenu = false;
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
id: content
|
||||
|
||||
anchors.margins: Metrics.radius(12)
|
||||
anchors.topMargin: Metrics.radius(16)
|
||||
anchors.leftMargin: Metrics.radius(18)
|
||||
anchors.fill: parent
|
||||
|
||||
Grid {
|
||||
columns: 3
|
||||
rows: 3
|
||||
rowSpacing: Metrics.spacing(10)
|
||||
columnSpacing: Metrics.spacing(10)
|
||||
anchors.fill: parent
|
||||
|
||||
PowerMenuButton {
|
||||
buttonIcon: "power_settings_new"
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["poweroff"]);
|
||||
Globals.visiblility.powermenu = false;
|
||||
}
|
||||
}
|
||||
|
||||
PowerMenuButton {
|
||||
buttonIcon: "logout"
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["hyprctl", "dispatch", "exit"]);
|
||||
Globals.visiblility.powermenu = false;
|
||||
}
|
||||
}
|
||||
|
||||
PowerMenuButton {
|
||||
buttonIcon: "sleep"
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["systemctl", "suspend"]);
|
||||
Globals.visiblility.powermenu = false;
|
||||
}
|
||||
}
|
||||
|
||||
PowerMenuButton {
|
||||
buttonIcon: "lock"
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["nucleus", "ipc", "call", "lockscreen", "lock"]);
|
||||
Globals.visiblility.powermenu = false;
|
||||
}
|
||||
}
|
||||
|
||||
PowerMenuButton {
|
||||
buttonIcon: "restart_alt"
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["reboot"]);
|
||||
Globals.visiblility.powermenu = false;
|
||||
}
|
||||
}
|
||||
|
||||
PowerMenuButton {
|
||||
buttonIcon: "light_off"
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["systemctl", "hibernate"]);
|
||||
Globals.visiblility.powermenu = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
component Anim: NumberAnimation {
|
||||
running: Config.runtime.appearance.animations.enabled
|
||||
duration: Metrics.chronoDuration(400)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.animation.curves.standard
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function toggle() {
|
||||
togglepowermenu();
|
||||
}
|
||||
|
||||
target: "powermenu"
|
||||
}
|
||||
|
||||
component PowerMenuButton: StyledButton {
|
||||
property string buttonIcon
|
||||
|
||||
icon: buttonIcon
|
||||
iconSize: Metrics.iconSize(50)
|
||||
width: powermenu.implicitWidth / 3.4
|
||||
height: powermenu.implicitHeight / 2.3
|
||||
radius: beingHovered ? Metrics.radius("verylarge") * 2 : Metrics.radius("large")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,606 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Effects
|
||||
import Quickshell.Wayland
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
property bool active: false
|
||||
property rect selectedRegion: Qt.rect(0, 0, 0, 0)
|
||||
property string tempScreenshot: ""
|
||||
|
||||
IpcHandler {
|
||||
target: "screen"
|
||||
function capture() {
|
||||
if (root.active) {
|
||||
console.info("screencap", "already active");
|
||||
return;
|
||||
}
|
||||
console.info("screencap", "starting capture");
|
||||
root.active = true;
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
active: root.active
|
||||
component: PanelWindow {
|
||||
id: win
|
||||
property bool closing: false
|
||||
property bool ready: false
|
||||
property bool processing: false
|
||||
property bool windowMode: false
|
||||
property string savedPath: ""
|
||||
property bool savedSuccess: false
|
||||
|
||||
color: Appearance.m3colors.m3surface
|
||||
anchors { top: true; left: true; right: true; bottom: true }
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||
WlrLayershell.namespace: "nucleus:screencapture"
|
||||
|
||||
Component.onCompleted: {
|
||||
var ts = Qt.formatDateTime(new Date(), "yyyy-MM-dd_hh-mm-ss");
|
||||
root.tempScreenshot = "/tmp/screenshot_" + ts + ".png";
|
||||
}
|
||||
|
||||
function close() {
|
||||
if (closing) return;
|
||||
closing = true;
|
||||
closeAnim.start();
|
||||
}
|
||||
|
||||
function saveFullscreen() {
|
||||
console.info("screencap", "saveFullscreen started");
|
||||
win.processing = true;
|
||||
screencopy.grabToImage(function(result) {
|
||||
console.info("screencap", "fullscreen grabbed");
|
||||
var ts = Qt.formatDateTime(new Date(), "yyyy-MM-dd_hh-mm-ss");
|
||||
win.savedPath = Quickshell.env("HOME") + "/Pictures/Screenshots/screenshot_" + ts + ".png";
|
||||
|
||||
console.info("screencap", "saving to: " + win.savedPath);
|
||||
if (result.saveToFile(win.savedPath)) {
|
||||
console.info("screencap", "saved, copying");
|
||||
Quickshell.execDetached({
|
||||
command: ["sh", "-c", "cat '" + win.savedPath + "' | wl-copy --type image/png"]
|
||||
});
|
||||
win.savedSuccess = true;
|
||||
} else {
|
||||
console.info("screencap", "save failed");
|
||||
win.savedSuccess = false;
|
||||
}
|
||||
win.processing = false;
|
||||
console.info("screencap", "closing window");
|
||||
win.close();
|
||||
});
|
||||
}
|
||||
|
||||
Component {
|
||||
id: ffmpegProc
|
||||
Process {
|
||||
property string outputPath
|
||||
property bool success: false
|
||||
|
||||
onExited: (code) => {
|
||||
console.info("screencap", "ffmpeg exited: " + code);
|
||||
success = code === 0;
|
||||
|
||||
if (success) {
|
||||
console.info("screencap", "copying to clipboard");
|
||||
Quickshell.execDetached({
|
||||
command: ["sh", "-c", "cat '" + outputPath + "' | wl-copy --type image/png"]
|
||||
});
|
||||
}
|
||||
|
||||
Quickshell.execDetached({ command: ["rm", root.tempScreenshot] });
|
||||
|
||||
win.savedSuccess = success;
|
||||
win.processing = false;
|
||||
console.info("screencap", "done, closing");
|
||||
win.close();
|
||||
destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function saveRegion(rect, suffix) {
|
||||
console.info("screencap", "saveRegion started: " + rect.x + "," + rect.y + " " + rect.width + "x" + rect.height);
|
||||
screencopy.grabToImage(function(result) {
|
||||
console.info("screencap", "full screenshot grabbed for cropping");
|
||||
if (!result.saveToFile(root.tempScreenshot)) {
|
||||
console.info("screencap", "ERROR: failed to save temp screenshot");
|
||||
win.savedSuccess = false;
|
||||
win.processing = false;
|
||||
win.close();
|
||||
return;
|
||||
}
|
||||
|
||||
console.info("screencap", "temp saved, cropping with ffmpeg");
|
||||
var ts = Qt.formatDateTime(new Date(), "yyyy-MM-dd_hh-mm-ss");
|
||||
win.savedPath = Quickshell.env("HOME") + "/Pictures/Screenshots/screenshot_" + ts + suffix + ".png";
|
||||
|
||||
ffmpegProc.createObject(win, {
|
||||
command: ["ffmpeg", "-i", root.tempScreenshot, "-vf", "crop=" + Math.floor(rect.width) + ":" + Math.floor(rect.height) + ":" + Math.floor(rect.x) + ":" + Math.floor(rect.y), "-y", win.savedPath],
|
||||
outputPath: win.savedPath,
|
||||
running: true
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function captureFullscreen() {
|
||||
win.processing = true;
|
||||
saveFullscreen();
|
||||
}
|
||||
|
||||
function captureWindow(rect) {
|
||||
win.processing = true;
|
||||
saveRegion(rect, "_window");
|
||||
}
|
||||
|
||||
function captureRegion() {
|
||||
if (!ready || !selection.hasSelection) return;
|
||||
win.processing = true;
|
||||
saveRegion(root.selectedRegion, "_region");
|
||||
}
|
||||
|
||||
ScreencopyView {
|
||||
id: screencopy
|
||||
anchors.fill: parent
|
||||
captureSource: win.screen
|
||||
z: -999
|
||||
live: false
|
||||
|
||||
onHasContentChanged: {
|
||||
console.info("screencap", "hasContent: " + hasContent);
|
||||
if (hasContent) {
|
||||
console.info("screencap", "grabbing for preview");
|
||||
grabToImage(function(result) {
|
||||
console.info("screencap", "preview grabbed: " + result.url);
|
||||
frozen.source = result.url;
|
||||
readyTimer.start();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: readyTimer
|
||||
interval: Metrics.chronoDuration("normal") + 50
|
||||
onTriggered: {
|
||||
console.info("screencap", "UI ready");
|
||||
win.ready = true;
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
|
||||
Keys.onEscapePressed: win.close()
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_F) {
|
||||
win.captureFullscreen();
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_W) {
|
||||
win.windowMode = !win.windowMode;
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
id: bg
|
||||
anchors.fill: parent
|
||||
source: Config.runtime.appearance.background.path
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
opacity: 0
|
||||
scale: 1
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
blurEnabled: true
|
||||
blur: 1.0
|
||||
blurMax: 64
|
||||
brightness: -0.1
|
||||
}
|
||||
|
||||
onStatusChanged: {
|
||||
if (status === Image.Ready) fadeIn.start();
|
||||
}
|
||||
|
||||
NumberAnimation on opacity {
|
||||
id: fadeIn
|
||||
to: 1
|
||||
duration: Metrics.chronoDuration("normal")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: container
|
||||
anchors.centerIn: parent
|
||||
width: win.width
|
||||
height: win.height
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
shadowEnabled: true
|
||||
shadowOpacity: 1
|
||||
shadowColor: Appearance.m3colors.m3shadow
|
||||
}
|
||||
|
||||
Image {
|
||||
id: frozen
|
||||
anchors.fill: parent
|
||||
fillMode: Image.PreserveAspectFit
|
||||
smooth: true
|
||||
cache: false
|
||||
}
|
||||
|
||||
Item {
|
||||
id: darkOverlay
|
||||
anchors.fill: parent
|
||||
visible: (selection.hasSelection || selection.selecting) && !win.windowMode
|
||||
|
||||
Rectangle {
|
||||
y: 0
|
||||
width: parent.width
|
||||
height: selection.sy
|
||||
color: "black"
|
||||
opacity: 0.5
|
||||
}
|
||||
Rectangle {
|
||||
y: selection.sy + selection.h
|
||||
width: parent.width
|
||||
height: parent.height - (selection.sy + selection.h)
|
||||
color: "black"
|
||||
opacity: 0.5
|
||||
}
|
||||
Rectangle {
|
||||
x: 0
|
||||
y: selection.sy
|
||||
width: selection.sx
|
||||
height: selection.h
|
||||
color: "black"
|
||||
opacity: 0.5
|
||||
}
|
||||
Rectangle {
|
||||
x: selection.sx + selection.w
|
||||
y: selection.sy
|
||||
width: parent.width - (selection.sx + selection.w)
|
||||
height: selection.h
|
||||
color: "black"
|
||||
opacity: 0.5
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
x: selection.sx
|
||||
y: selection.sy
|
||||
width: selection.w
|
||||
height: selection.h
|
||||
color: "black"
|
||||
opacity: win.processing ? 0.6 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: 200 }
|
||||
}
|
||||
|
||||
LoadingIcon {
|
||||
anchors.centerIn: parent
|
||||
visible: win.processing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: outline
|
||||
x: selection.sx
|
||||
y: selection.sy
|
||||
width: selection.w
|
||||
height: selection.h
|
||||
color: "transparent"
|
||||
border.color: Appearance.m3colors.m3primary
|
||||
border.width: 2
|
||||
visible: (selection.selecting || selection.hasSelection) && !win.windowMode
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: selection.selecting
|
||||
anchors.top: outline.bottom
|
||||
anchors.topMargin: Metrics.margin(10)
|
||||
anchors.horizontalCenter: outline.horizontalCenter
|
||||
width: coords.width + 10
|
||||
height: coords.height + 10
|
||||
color: Appearance.m3colors.m3surface
|
||||
radius: Metrics.radius(20)
|
||||
|
||||
StyledText {
|
||||
id: coords
|
||||
anchors.centerIn: parent
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
animate: false
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
property real scaleX: container.width / win.width
|
||||
property real scaleY: container.height / win.height
|
||||
text: Math.floor(selection.sx/scaleX) + "," + Math.floor(selection.sy/scaleY) + " " + Math.floor(selection.w/scaleX) + "x" + Math.floor(selection.h/scaleY)
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: selection
|
||||
anchors.fill: parent
|
||||
enabled: win.ready && !win.windowMode
|
||||
|
||||
property real x1: 0
|
||||
property real y1: 0
|
||||
property real x2: 0
|
||||
property real y2: 0
|
||||
property bool selecting: false
|
||||
property bool hasSelection: false
|
||||
|
||||
property real xp: 0
|
||||
property real yp: 0
|
||||
property real wp: 0
|
||||
property real hp: 0
|
||||
|
||||
property real sx: xp * parent.width
|
||||
property real sy: yp * parent.height
|
||||
property real w: wp * parent.width
|
||||
property real h: hp * parent.height
|
||||
|
||||
onPressed: mouse => {
|
||||
if (!win.ready) return;
|
||||
x1 = Math.max(0, Math.min(mouse.x, width));
|
||||
y1 = Math.max(0, Math.min(mouse.y, height));
|
||||
x2 = x1;
|
||||
y2 = y1;
|
||||
selecting = true;
|
||||
hasSelection = false;
|
||||
}
|
||||
|
||||
onPositionChanged: mouse => {
|
||||
if (selecting) {
|
||||
x2 = Math.max(0, Math.min(mouse.x, width));
|
||||
y2 = Math.max(0, Math.min(mouse.y, height));
|
||||
xp = Math.min(x1, x2) / width;
|
||||
yp = Math.min(y1, y2) / height;
|
||||
wp = Math.abs(x2 - x1) / width;
|
||||
hp = Math.abs(y2 - y1) / height;
|
||||
}
|
||||
}
|
||||
|
||||
onReleased: mouse => {
|
||||
if (!selecting) return;
|
||||
|
||||
x2 = Math.max(0, Math.min(mouse.x, width));
|
||||
y2 = Math.max(0, Math.min(mouse.y, height));
|
||||
selecting = false;
|
||||
|
||||
hasSelection = Math.abs(x2 - x1) > 5 && Math.abs(y2 - y1) > 5;
|
||||
|
||||
if (hasSelection) {
|
||||
xp = Math.min(x1, x2) / width;
|
||||
yp = Math.min(y1, y2) / height;
|
||||
wp = Math.abs(x2 - x1) / width;
|
||||
hp = Math.abs(y2 - y1) / height;
|
||||
|
||||
root.selectedRegion = Qt.rect(
|
||||
Math.min(x1, x2) * win.screen.width / width,
|
||||
Math.min(y1, y2) * win.screen.height / height,
|
||||
Math.abs(x2 - x1) * win.screen.width / width,
|
||||
Math.abs(y2 - y1) * win.screen.height / height
|
||||
);
|
||||
|
||||
win.captureRegion();
|
||||
} else {
|
||||
win.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: {
|
||||
if (!win.windowMode || !win.ready) return [];
|
||||
var ws = Hyprland.focusedMonitor?.activeWorkspace;
|
||||
return ws?.toplevels ? ws.toplevels.values : [];
|
||||
}
|
||||
|
||||
delegate: Item {
|
||||
required property var modelData
|
||||
property var w: modelData?.lastIpcObject
|
||||
visible: w?.at && w?.size
|
||||
|
||||
property real barX: 0
|
||||
property real barY: 0
|
||||
property real sx: container.width / (win.screen.width - barX)
|
||||
property real sy: container.height / (win.screen.height - barY)
|
||||
|
||||
x: visible ? (w.at[0] - barX) * sx : 0
|
||||
y: visible ? (w.at[1] - barY) * sy : 0
|
||||
width: visible ? w.size[0] * sx : 0
|
||||
height: visible ? w.size[1] * sy : 0
|
||||
z: w?.floating ? (hover.containsMouse ? 1000 : 100) : (hover.containsMouse ? 50 : 0)
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "transparent"
|
||||
border.color: Appearance.m3colors.m3primary
|
||||
border.width: hover.containsMouse ? 3 : 0
|
||||
radius: Metrics.radius(8)
|
||||
Behavior on border.width {
|
||||
NumberAnimation { duration: Metrics.chronoDuration(150) }
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Appearance.m3colors.m3primary
|
||||
opacity: hover.containsMouse ? 0.15 : 0
|
||||
radius: Metrics.radius(8)
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: Metrics.chronoDuration(150) }
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: hover
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
win.captureWindow(Qt.rect(w.at[0], w.at[1], w.size[0], w.size[1]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
running: win.visible && !win.closing && frozen.source != ""
|
||||
|
||||
NumberAnimation {
|
||||
target: bg
|
||||
property: "scale"
|
||||
to: bg.scale + 0.05
|
||||
duration: Metrics.chronoDuration("normal")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
NumberAnimation {
|
||||
target: container
|
||||
property: "width"
|
||||
to: win.width * 0.8
|
||||
duration: Metrics.chronoDuration("normal")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
NumberAnimation {
|
||||
target: container
|
||||
property: "height"
|
||||
to: win.height * 0.8
|
||||
duration: Metrics.chronoDuration("normal")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
id: closeAnim
|
||||
|
||||
NumberAnimation {
|
||||
target: bg
|
||||
property: "scale"
|
||||
to: bg.scale - 0.05
|
||||
duration: Metrics.chronoDuration("normal")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
NumberAnimation {
|
||||
target: container
|
||||
property: "width"
|
||||
to: win.width
|
||||
duration: Metrics.chronoDuration("normal")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
NumberAnimation {
|
||||
target: container
|
||||
property: "height"
|
||||
to: win.height
|
||||
duration: Metrics.chronoDuration("normal")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
NumberAnimation {
|
||||
target: darkOverlay
|
||||
property: "opacity"
|
||||
to: 0
|
||||
duration: Metrics.chronoDuration("normal")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
NumberAnimation {
|
||||
target: outline
|
||||
property: "opacity"
|
||||
to: 0
|
||||
duration: Metrics.chronoDuration("normal")
|
||||
easing.type: Appearance.animation.easing
|
||||
}
|
||||
|
||||
onFinished: {
|
||||
root.active = false;
|
||||
if (win.savedSuccess) {
|
||||
Quickshell.execDetached({
|
||||
command: ["notify-send", "Screenshot saved", win.savedPath.split("/").pop() + " (copied)"]
|
||||
});
|
||||
} else if (win.savedPath !== "") {
|
||||
Quickshell.execDetached({
|
||||
command: ["notify-send", "Screenshot failed", "Could not save"]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: Metrics.margin(30)
|
||||
width: row.width + 20
|
||||
height: row.height + 20
|
||||
visible: true
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Appearance.m3colors.m3surface
|
||||
radius: Metrics.radius("large")
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: row
|
||||
anchors.centerIn: parent
|
||||
|
||||
StyledButton {
|
||||
icon: "fullscreen"
|
||||
text: "Full screen"
|
||||
tooltipText: "Capture the whole screen [F]"
|
||||
onClicked: win.captureFullscreen()
|
||||
}
|
||||
Rectangle {
|
||||
Layout.fillHeight: true
|
||||
width: 2
|
||||
color: Appearance.m3colors.m3onSurfaceVariant
|
||||
opacity: 0.2
|
||||
}
|
||||
StyledButton {
|
||||
icon: "window"
|
||||
checkable: true
|
||||
checked: win.windowMode
|
||||
text: "Window"
|
||||
tooltipText: "Hover and click a window [W]"
|
||||
onClicked: win.windowMode = !win.windowMode
|
||||
}
|
||||
StyledButton {
|
||||
secondary: true
|
||||
icon: "close"
|
||||
tooltipText: "Exit [Escape]"
|
||||
onClicked: win.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HyprlandFocusGrab {
|
||||
id: grab
|
||||
windows: [win]
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) grab.active = true
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: grab
|
||||
function onActiveChanged() {
|
||||
if (!grab.active && !win.closing) win.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.config
|
||||
import qs.services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
property int logoOffset: -30
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
width: 460
|
||||
spacing: Metrics.spacing(12)
|
||||
Item {
|
||||
width: parent.width
|
||||
height: Metrics.fontSize(200)
|
||||
|
||||
StyledText {
|
||||
text: SystemDetails.osIcon
|
||||
anchors.centerIn: parent
|
||||
x: root.logoOffset
|
||||
font.pixelSize: Metrics.fontSize(200)
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Nucleus Shell"
|
||||
width: parent.width
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font.family: "Outfit ExtraBold"
|
||||
font.pixelSize: Metrics.fontSize(26)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "A shell built to get things done."
|
||||
width: parent.width
|
||||
wrapMode: Text.Wrap
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Metrics.spacing(10)
|
||||
|
||||
StyledButton {
|
||||
text: "View on GitHub"
|
||||
icon: "code"
|
||||
secondary: true
|
||||
onClicked: Qt.openUrlExternally("https://github.com/xZepyx/nucleus-shell")
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
text: "Report Issue"
|
||||
icon: "bug_report"
|
||||
secondary: true
|
||||
onClicked: Qt.openUrlExternally("https://github.com/xZepyx/nucleus-shell/issues")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Nucleus-Shell v" + Config.runtime.shell.version
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: Metrics.margin(24)
|
||||
font.pixelSize: Metrics.fontSize(12)
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: 52
|
||||
height: 52
|
||||
radius: Appearance.rounding.small
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: Metrics.margin(24)
|
||||
|
||||
StyledText {
|
||||
text: "↻"
|
||||
anchors.centerIn: parent
|
||||
font.pixelSize: Metrics.fontSize(22)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
|
||||
onClicked: {
|
||||
Globals.states.settingsOpen = false
|
||||
|
||||
Quickshell.execDetached(["notify-send", "Updating Nucleus Shell"])
|
||||
|
||||
Quickshell.execDetached([
|
||||
"kitty",
|
||||
"--hold",
|
||||
"bash",
|
||||
"-c",
|
||||
Directories.scriptsPath + "/system/update.sh"
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,364 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
import qs.plugins
|
||||
|
||||
ContentMenu {
|
||||
title: "Appearance"
|
||||
description: "Adjust how the desktop looks like."
|
||||
|
||||
ContentCard {
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(16)
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Metrics.spacing(4)
|
||||
|
||||
StyledText {
|
||||
text: "Select Theme"
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Choose between dark or light mode."
|
||||
font.pixelSize: Metrics.fontSize(12)
|
||||
color: "#888888"
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.leftMargin: Metrics.margin(15)
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: Metrics.spacing(16)
|
||||
|
||||
StyledButton {
|
||||
Layout.preferredHeight: 300
|
||||
Layout.preferredWidth: 460
|
||||
Layout.maximumHeight: 400
|
||||
Layout.maximumWidth: 500
|
||||
icon: "dark_mode"
|
||||
iconSize: Metrics.iconSize(64)
|
||||
checked: Config.runtime.appearance.theme === "dark"
|
||||
hoverEnabled: true
|
||||
|
||||
onClicked: {
|
||||
if (!Config.runtime.appearance.colors.autogenerated) {
|
||||
const scheme = Config.runtime.appearance.colors.scheme
|
||||
const file = Theme.map[scheme]?.dark
|
||||
if (!file) {
|
||||
Theme.notifyMissingVariant(scheme, "dark")
|
||||
return
|
||||
}
|
||||
|
||||
Config.updateKey("appearance.theme", "dark")
|
||||
Quickshell.execDetached([
|
||||
"nucleus", "theme", "switch", file
|
||||
])
|
||||
} else {
|
||||
Config.updateKey("appearance.theme", "dark")
|
||||
Quickshell.execDetached([
|
||||
"nucleus", "ipc", "call", "global", "regenColors"
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
Layout.preferredHeight: 300
|
||||
Layout.preferredWidth: 460
|
||||
Layout.maximumHeight: 400
|
||||
Layout.maximumWidth: 500
|
||||
icon: "light_mode"
|
||||
iconSize: Metrics.iconSize(64)
|
||||
checked: Config.runtime.appearance.theme === "light"
|
||||
hoverEnabled: true
|
||||
|
||||
onClicked: {
|
||||
if (!Config.runtime.appearance.colors.autogenerated) {
|
||||
const scheme = Config.runtime.appearance.colors.scheme
|
||||
const file = Theme.map[scheme]?.light
|
||||
if (!file) {
|
||||
Theme.notifyMissingVariant(scheme, "light")
|
||||
return
|
||||
}
|
||||
|
||||
Config.updateKey("appearance.theme", "light")
|
||||
Quickshell.execDetached([
|
||||
"nucleus", "theme", "switch", file
|
||||
])
|
||||
} else {
|
||||
Config.updateKey("appearance.theme", "light")
|
||||
Quickshell.execDetached([
|
||||
"nucleus", "ipc", "call", "global", "regenColors"
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: Metrics.spacing(30)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
RowLayout {
|
||||
opacity: autogeneratedColorsSelector.enabled ? 1 : 0.8
|
||||
|
||||
ColumnLayout {
|
||||
StyledText {
|
||||
text: "Color Generation Schemes:"
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Choose the scheme for autogenerated color generation."
|
||||
font.pixelSize: Metrics.fontSize(12)
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
StyledDropDown {
|
||||
id: autogeneratedColorsSelector
|
||||
label: "Color Scheme"
|
||||
model: [
|
||||
"scheme-content",
|
||||
"scheme-expressive",
|
||||
"scheme-fidelity",
|
||||
"scheme-fruit-salad",
|
||||
"scheme-monochrome",
|
||||
"scheme-neutral",
|
||||
"scheme-rainbow",
|
||||
"scheme-tonal-spot"
|
||||
]
|
||||
|
||||
currentIndex: model.indexOf(Config.runtime.appearance.colors.matugenScheme)
|
||||
|
||||
onSelectedIndexChanged: (index) => {
|
||||
if (!Config.runtime.appearance.colors.autogenerated)
|
||||
return
|
||||
const selectedScheme = model[index]
|
||||
Config.updateKey("appearance.colors.matugenScheme", selectedScheme)
|
||||
Quickshell.execDetached([
|
||||
"nucleus", "ipc", "call", "global", "regenColors"
|
||||
])
|
||||
}
|
||||
|
||||
enabled: Config.runtime.appearance.colors.autogenerated
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
opacity: predefinedThemeSelector.enabled ? 1 : 0.8
|
||||
|
||||
ColumnLayout {
|
||||
StyledText {
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
text: "Predefined/Custom Themes:"
|
||||
}
|
||||
|
||||
StyledText {
|
||||
font.pixelSize: Metrics.fontSize(12)
|
||||
text: "Choose a pre-defined theme for your interface."
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
StyledDropDown {
|
||||
id: predefinedThemeSelector
|
||||
label: "Theme"
|
||||
model: Object.keys(Theme.map)
|
||||
currentIndex: model.indexOf(Config.runtime.appearance.colors.scheme)
|
||||
|
||||
onSelectedIndexChanged: (index) => {
|
||||
if (Config.runtime.appearance.colors.autogenerated)
|
||||
return
|
||||
const selectedTheme = model[index]
|
||||
const variant = Config.runtime.appearance.theme
|
||||
const file = Theme.map[selectedTheme][variant]
|
||||
if (!file) return
|
||||
|
||||
Config.updateKey("appearance.colors.scheme", selectedTheme)
|
||||
Quickshell.execDetached([
|
||||
"nucleus", "theme", "switch", file
|
||||
])
|
||||
}
|
||||
|
||||
enabled: !Config.runtime.appearance.colors.autogenerated
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
StyledSwitchOption {
|
||||
title: "Tint Icons"
|
||||
description: "Either tint icons across the shell or keep them colorized."
|
||||
prefField: "appearance.tintIcons"
|
||||
}
|
||||
|
||||
StyledSwitchOption {
|
||||
title: "Use Autogenerated Themes"
|
||||
description: "Use autogenerated themes."
|
||||
prefField: "appearance.colors.autogenerated"
|
||||
}
|
||||
|
||||
StyledSwitchOption {
|
||||
title: "Use User Defined Themes"
|
||||
description: "Enabling this will also run the default `config.toml` in `~/.config/matugen` dir."
|
||||
prefField: "appearance.colors.runMatugenUserWide"
|
||||
}
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
StyledText {
|
||||
text: "Clock"
|
||||
font.pixelSize: Metrics.fontSize(20)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
StyledSwitchOption {
|
||||
title: "Show Clock"
|
||||
description: "Whether to show or disable the clock on the background."
|
||||
prefField: "appearance.background.clock.enabled"
|
||||
}
|
||||
|
||||
StyledSwitchOption {
|
||||
title: "Analog Variant"
|
||||
description: "Whether to use analog clock or not."
|
||||
prefField: "appearance.background.clock.isAnalog"
|
||||
}
|
||||
|
||||
StyledSwitchOption {
|
||||
title: "Rotate Clock Polygon"
|
||||
description: "Rotate the shape polygon of the analog clock."
|
||||
prefField: "appearance.background.clock.rotatePolygonBg"
|
||||
enabled: Config.runtime.appearance.background.clock.isAnalog
|
||||
opacity: enabled ? 1 : 0.8
|
||||
}
|
||||
|
||||
NumberStepper {
|
||||
label: "Rotation Duration"
|
||||
description: "Adjust the duration in which the clock rotates 360* (Seconds)."
|
||||
prefField: "appearance.background.clock.rotationDuration"
|
||||
minimum: 1
|
||||
maximum: 40
|
||||
step: 1
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: shapeSelector
|
||||
|
||||
ColumnLayout {
|
||||
StyledText {
|
||||
text: "Analog Clock Shape"
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Choose the analog clock's shape."
|
||||
font.pixelSize: Metrics.fontSize(12)
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
StyledDropDown {
|
||||
label: "Shape Type"
|
||||
model: ["Cookie 7 Sided", "Cookie 9 Sided", "Cookie 12 Sided", "Pixelated Circle", "Circle"]
|
||||
|
||||
currentIndex: Config.runtime.appearance.background.clock.shape
|
||||
|
||||
onSelectedIndexChanged: (index) => {
|
||||
Config.updateKey(
|
||||
"appearance.background.clock.shape",
|
||||
index
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
StyledText {
|
||||
text: "Rounding"
|
||||
font.pixelSize: Metrics.fontSize(20)
|
||||
font.bold: true
|
||||
}
|
||||
NumberStepper {
|
||||
label: "Factor"
|
||||
description: "Adjust the rounding factor."
|
||||
prefField: "appearance.rounding.factor"
|
||||
minimum: 0
|
||||
maximum: 1
|
||||
step: 0.1
|
||||
}
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
StyledText {
|
||||
text: "Font"
|
||||
font.pixelSize: Metrics.fontSize(20)
|
||||
font.bold: true
|
||||
}
|
||||
NumberStepper {
|
||||
label: "Scale"
|
||||
description: "Adjust the font scale."
|
||||
prefField: "appearance.font.scale"
|
||||
minimum: 0.1
|
||||
maximum: 2
|
||||
step: 0.1
|
||||
}
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
StyledText {
|
||||
text: "Transparency"
|
||||
font.pixelSize: Metrics.fontSize(20)
|
||||
font.bold: true
|
||||
}
|
||||
StyledSwitchOption {
|
||||
title: "Enabled"
|
||||
description: "Whether to enable or disable transparency."
|
||||
prefField: "appearance.transparency.enabled"
|
||||
}
|
||||
NumberStepper {
|
||||
label: "Factor"
|
||||
description: "Adjust the alpha value for transparency."
|
||||
prefField: "appearance.transparency.alpha"
|
||||
minimum: 0.1
|
||||
maximum: 1
|
||||
step: 0.1
|
||||
}
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
StyledText {
|
||||
text: "Animations"
|
||||
font.pixelSize: Metrics.fontSize(20)
|
||||
font.bold: true
|
||||
}
|
||||
StyledSwitchOption {
|
||||
title: "Enabled"
|
||||
description: "Whether to enable or disable animations (applies everywhere in the shell)."
|
||||
prefField: "appearance.animations.enabled"
|
||||
}
|
||||
NumberStepper {
|
||||
label: "Duration Scale"
|
||||
description: "Adjust the duration scale of the animations."
|
||||
prefField: "appearance.animations.durationScale"
|
||||
minimum: 0.1
|
||||
maximum: 1
|
||||
step: 0.1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Io
|
||||
import qs.modules.functions
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
|
||||
ContentMenu {
|
||||
title: "Sound"
|
||||
description: "Volume and audio devices"
|
||||
|
||||
ContentCard {
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(20)
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(16)
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 40
|
||||
Layout.preferredHeight: 40
|
||||
radius: Metrics.radius("large")
|
||||
color: Appearance.m3colors.m3primaryContainer
|
||||
|
||||
MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
icon: "volume_up"
|
||||
color: Appearance.m3colors.m3onPrimaryContainer
|
||||
iconSize: Metrics.iconSize(24)
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
StyledText {
|
||||
text: "Output"
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
font.family: Metrics.fontFamily("Outfit Medium")
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: Volume.defaultSink.description
|
||||
font.pixelSize: Metrics.fontSize(13)
|
||||
color: Appearance.m3colors.m3onSurfaceVariant
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 1
|
||||
color: Appearance.m3colors.m3outlineVariant
|
||||
opacity: 0.4
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(12)
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
StyledText {
|
||||
text: "Volume"
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
font.family: Metrics.fontFamily("Outfit Medium")
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
StyledText {
|
||||
animate: false
|
||||
text: Math.round(Volume.defaultSink.audio.volume * 100) + "%"
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
font.family: Metrics.fontFamily("Outfit SemiBold")
|
||||
color: Appearance.m3colors.m3primary
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(16)
|
||||
|
||||
MaterialSymbol {
|
||||
icon: Volume.defaultSink.audio.muted ? "volume_off"
|
||||
: Volume.defaultSink.audio.volume < 0.33 ? "volume_mute"
|
||||
: Volume.defaultSink.audio.volume < 0.66 ? "volume_down"
|
||||
: "volume_up"
|
||||
color: Appearance.m3colors.m3onSurfaceVariant
|
||||
iconSize: Metrics.iconSize(24)
|
||||
}
|
||||
|
||||
StyledSlider {
|
||||
id: outputVolumeSlider
|
||||
Layout.fillWidth: true
|
||||
value: Volume.defaultSink.audio.volume * 100
|
||||
onValueChanged: Volume.setVolume(value / 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(8)
|
||||
|
||||
StyledText {
|
||||
text: "Device"
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
font.family: Metrics.fontFamily("Outfit Medium")
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
}
|
||||
|
||||
StyledDropDown {
|
||||
Layout.fillWidth: true
|
||||
label: "Output device"
|
||||
model: Volume.sinks.map(d => d.description)
|
||||
currentIndex: {
|
||||
for (let i = 0; i < Volume.sinks.length; i++)
|
||||
if (Volume.sinks[i].name === Volume.defaultSink.name) return i
|
||||
return -1
|
||||
}
|
||||
onSelectedIndexChanged: index => {
|
||||
if (index >= 0 && index < Volume.sinks.length)
|
||||
Volume.setDefaultSink(Volume.sinks[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 56
|
||||
radius: Metrics.radius("small")
|
||||
color: Appearance.m3colors.m3surfaceContainerHigh
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Metrics.margin(16)
|
||||
anchors.rightMargin: Metrics.margin(16)
|
||||
spacing: Metrics.spacing(12)
|
||||
|
||||
MaterialSymbol {
|
||||
icon: Volume.defaultSink.audio.muted ? "volume_off" : "volume_up"
|
||||
color: Volume.defaultSink.audio.muted ? Appearance.m3colors.m3error : Appearance.m3colors.m3onSurfaceVariant
|
||||
iconSize: Metrics.iconSize(24)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: "Mute output"
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
}
|
||||
|
||||
StyledSwitch {
|
||||
checked: Volume.defaultSink.audio.muted
|
||||
onToggled: Volume.toggleMuted(Volume.defaultSink)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(20)
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(16)
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 40
|
||||
Layout.preferredHeight: 40
|
||||
radius: Metrics.radius("large")
|
||||
color: Appearance.m3colors.m3secondaryContainer
|
||||
|
||||
MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
icon: "mic"
|
||||
color: Appearance.m3colors.m3onSecondaryContainer
|
||||
iconSize: Metrics.iconSize(24)
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(2)
|
||||
|
||||
StyledText {
|
||||
text: "Input"
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
font.family: Metrics.fontFamily("Outfit Medium")
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: Volume.sources.length > 0
|
||||
text: Volume.defaultSource.description
|
||||
font.pixelSize: Metrics.fontSize(13)
|
||||
color: Appearance.m3colors.m3onSurfaceVariant
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: Volume.sources.length === 0
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 120
|
||||
radius: Metrics.radius("small")
|
||||
color: Appearance.m3colors.m3surfaceContainerHigh
|
||||
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: Metrics.spacing(8)
|
||||
|
||||
MaterialSymbol {
|
||||
icon: "mic_off"
|
||||
iconSize: Metrics.iconSize(48)
|
||||
color: ColorUtils.transparentize(Appearance.m3colors.m3onSurface, 0.3)
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "No input devices"
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
font.family: Metrics.fontFamily("Outfit Medium")
|
||||
color: Appearance.m3colors.m3onSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: Volume.sources.length > 0
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 1
|
||||
color: Appearance.m3colors.m3outlineVariant
|
||||
opacity: 0.4
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
visible: Volume.sources.length > 0
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(12)
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
StyledText {
|
||||
text: "Volume"
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
font.family: Metrics.fontFamily("Outfit Medium")
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
StyledText {
|
||||
animate: false
|
||||
text: Math.round(Volume.defaultSource.audio.volume * 100) + "%"
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
font.family: Metrics.fontFamily("Outfit SemiBold")
|
||||
color: Appearance.m3colors.m3primary
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(16)
|
||||
|
||||
MaterialSymbol {
|
||||
icon: Volume.defaultSource.audio.muted ? "mic_off" : "mic"
|
||||
color: Appearance.m3colors.m3onSurfaceVariant
|
||||
iconSize: Metrics.iconSize(24)
|
||||
}
|
||||
|
||||
StyledSlider {
|
||||
id: inputVolumeSlider
|
||||
Layout.fillWidth: true
|
||||
value: Volume.defaultSource.audio.volume * 100
|
||||
onValueChanged: Volume.setSourceVolume(value / 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
visible: Volume.sources.length > 0
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(8)
|
||||
|
||||
StyledText {
|
||||
text: "Device"
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
font.family: Metrics.fontFamily("Outfit Medium")
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
}
|
||||
|
||||
StyledDropDown {
|
||||
Layout.fillWidth: true
|
||||
label: "Input device"
|
||||
model: Volume.sources.map(d => d.description)
|
||||
currentIndex: {
|
||||
for (let i = 0; i < Volume.sources.length; i++)
|
||||
if (Volume.sources[i].name === Volume.defaultSource.name) return i
|
||||
return -1
|
||||
}
|
||||
onSelectedIndexChanged: index => {
|
||||
if (index >= 0 && index < Volume.sources.length)
|
||||
Volume.setDefaultSource(Volume.sources[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: Volume.sources.length > 0
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 56
|
||||
radius: Metrics.radius("small")
|
||||
color: Appearance.m3colors.m3surfaceContainerHigh
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Metrics.margin(16)
|
||||
anchors.rightMargin: Metrics.margin(16)
|
||||
spacing: Metrics.spacing(12)
|
||||
|
||||
MaterialSymbol {
|
||||
icon: Volume.defaultSource.audio.muted ? "mic_off" : "mic"
|
||||
color: Volume.defaultSource.audio.muted ? Appearance.m3colors.m3error : Appearance.m3colors.m3onSurfaceVariant
|
||||
iconSize: Metrics.iconSize(24)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: "Mute input"
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
}
|
||||
|
||||
StyledSwitch {
|
||||
checked: Volume.defaultSource.audio.muted
|
||||
onToggled: Volume.toggleMuted(Volume.defaultSource)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick.Controls
|
||||
import Quickshell.Widgets
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
|
||||
ContentMenu {
|
||||
property string barKey: "bar"
|
||||
title: "Bar"
|
||||
description: "Adjust the bar's look."
|
||||
|
||||
ContentCard {
|
||||
id: monitorSelectorCard
|
||||
|
||||
StyledText {
|
||||
text: "Monitor Bar Configuration"
|
||||
font.pixelSize: Metrics.fontSize(20)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: (Config.runtime.monitors?.[monitorSelector.model[monitorSelector.currentIndex]]?.bar)
|
||||
? "This monitor has its own bar configuration."
|
||||
: "This monitor currently uses the global bar."
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Metrics.spacing("normal")
|
||||
|
||||
StyledDropDown {
|
||||
id: monitorSelector
|
||||
Layout.preferredWidth: 220
|
||||
model: Xrandr.monitors.map(m => m.name)
|
||||
currentIndex: 0
|
||||
onCurrentIndexChanged: monitorSelectorCard.updateMonitorProperties()
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
StyledButton {
|
||||
id: createButton
|
||||
icon: "add"
|
||||
text: "Override Bar: (" + monitorSelector.model[monitorSelector.currentIndex] + ")"
|
||||
Layout.preferredWidth: 280
|
||||
onClicked: {
|
||||
const monitorName = monitorSelector.model[monitorSelector.currentIndex]
|
||||
if (!monitorName) return
|
||||
if (!Config.runtime.monitors) Config.runtime.monitors = {}
|
||||
if (!Config.runtime.monitors[monitorName])
|
||||
Config.runtime.monitors[monitorName] = {}
|
||||
|
||||
const defaultBar = {
|
||||
density: 50,
|
||||
enabled: true,
|
||||
floating: false,
|
||||
gothCorners: true,
|
||||
margins: 16,
|
||||
merged: false,
|
||||
modules: {
|
||||
height: 34,
|
||||
paddingColor: "#1f1f1f",
|
||||
radius: 17,
|
||||
statusIcons: {
|
||||
bluetoothStatusEnabled: true,
|
||||
enabled: true,
|
||||
networkStatusEnabled: true
|
||||
},
|
||||
systemUsage: {
|
||||
cpuStatsEnabled: true,
|
||||
enabled: true,
|
||||
memoryStatsEnabled: true,
|
||||
tempStatsEnabled: true
|
||||
},
|
||||
workspaces: {
|
||||
enabled: true,
|
||||
showAppIcons: true,
|
||||
showJapaneseNumbers: false,
|
||||
workspaceIndicators: 8
|
||||
}
|
||||
},
|
||||
position: "top",
|
||||
radius: 23
|
||||
}
|
||||
|
||||
Config.updateKey("monitors." + monitorName + ".bar", defaultBar)
|
||||
monitorSelectorCard.updateMonitorProperties()
|
||||
}
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
id: deleteButton
|
||||
icon: "delete"
|
||||
text: "Use Global Bar: (" + monitorSelector.model[monitorSelector.currentIndex] + ")"
|
||||
secondary: true
|
||||
Layout.preferredWidth: 280
|
||||
onClicked: {
|
||||
const monitorName = monitorSelector.model[monitorSelector.currentIndex]
|
||||
if (!monitorName) return
|
||||
Config.updateKey("monitors." + monitorName + ".bar", undefined)
|
||||
monitorSelectorCard.updateMonitorProperties()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateMonitorProperties() {
|
||||
const monitorName = monitorSelector.model[monitorSelector.currentIndex]
|
||||
const monitorBar = Config.runtime.monitors?.[monitorName]?.bar
|
||||
barKey = monitorBar ? "monitors." + monitorName + ".bar" : "bar"
|
||||
|
||||
createButton.enabled = !monitorBar
|
||||
deleteButton.enabled = !!monitorBar
|
||||
|
||||
monitorSelector.model = Xrandr.monitors.map(m => m.name)
|
||||
monitorSelector.currentIndex = Xrandr.monitors.findIndex(m => m.name === monitorName)
|
||||
}
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
StyledText {
|
||||
text: "Bar"
|
||||
font.pixelSize: Metrics.fontSize(20)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
StyledText {
|
||||
text: "Position"
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Metrics.spacing(8)
|
||||
Repeater {
|
||||
model: ["Top", "Bottom", "Left", "Right"]
|
||||
delegate: StyledButton {
|
||||
property string pos: modelData.toLowerCase()
|
||||
text: modelData
|
||||
Layout.fillWidth: true
|
||||
checked: ConfigResolver.bar(monitorSelector.model[monitorSelector.currentIndex]).position === pos
|
||||
topLeftRadius: Metrics.radius("normal")
|
||||
topRightRadius: Metrics.radius("normal")
|
||||
bottomLeftRadius: Metrics.radius("normal")
|
||||
bottomRightRadius: Metrics.radius("normal")
|
||||
onClicked: Config.updateKey(barKey + ".position", pos)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledSwitchOption {
|
||||
title: "Enabled"
|
||||
description: "Toggle the bar visibility on/off"
|
||||
prefField: barKey + ".enabled"
|
||||
}
|
||||
StyledSwitchOption {
|
||||
title: "Floating Bar"
|
||||
description: "Make the bar float above other windows instead of being part of the desktop"
|
||||
prefField: barKey + ".floating"
|
||||
}
|
||||
StyledSwitchOption {
|
||||
title: "Goth Corners"
|
||||
description: "Apply gothic-style corner cutouts to the bar"
|
||||
prefField: barKey + ".gothCorners"
|
||||
}
|
||||
StyledSwitchOption {
|
||||
title: "Merged Layout"
|
||||
description: "Merge all modules into a single continuous layout"
|
||||
prefField: barKey + ".merged"
|
||||
}
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
StyledText {
|
||||
text: "Bar Rounding & Size"
|
||||
font.pixelSize: Metrics.fontSize(20)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
NumberStepper {
|
||||
label: "Bar Density"
|
||||
prefField: barKey + ".density"
|
||||
description: "Modify the bar's density"
|
||||
minimum: 40
|
||||
maximum: 128
|
||||
}
|
||||
NumberStepper {
|
||||
label: "Bar Radius"
|
||||
prefField: barKey + ".radius"
|
||||
description: "Modify the bar's radius"
|
||||
minimum: 10
|
||||
maximum: 128
|
||||
}
|
||||
NumberStepper {
|
||||
label: "Module Container Radius"
|
||||
prefField: barKey + ".modules.radius"
|
||||
description: "Modify the bar's module.radius"
|
||||
minimum: 10
|
||||
maximum: 128
|
||||
}
|
||||
NumberStepper {
|
||||
label: "Module Height"
|
||||
prefField: barKey + ".modules.height"
|
||||
description: "Modify the bar's module.height"
|
||||
minimum: 10
|
||||
maximum: 128
|
||||
}
|
||||
NumberStepper {
|
||||
label: "Workspace Indicators"
|
||||
prefField: barKey + ".modules.workspaces.workspaceIndicators"
|
||||
description: "Adjust how many workspace indicators to show."
|
||||
minimum: 1
|
||||
maximum: 10
|
||||
}
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
StyledText {
|
||||
text: "Bar Modules"
|
||||
font.pixelSize: Metrics.fontSize(20)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Workspaces"
|
||||
font.pixelSize: Metrics.fontSize(18)
|
||||
font.bold: true
|
||||
}
|
||||
StyledSwitchOption {
|
||||
title: "Enabled"
|
||||
description: "Show workspace indicator module"
|
||||
prefField: barKey + ".modules.workspaces.enabled"
|
||||
}
|
||||
StyledSwitchOption {
|
||||
title: "Show App Icons"
|
||||
description: "Display application icons in workspace indicators"
|
||||
prefField: barKey + ".modules.workspaces.showAppIcons"
|
||||
enabled: !barKey.modules.workspaces.showJapaneseNumbers && Compositor.require("hyprland")
|
||||
opacity: !barKey.modules.workspaces.showJapaneseNumbers && Compositor.require("hyprland") ? 1 : 0.8
|
||||
}
|
||||
StyledSwitchOption {
|
||||
title: "Show Japanese Numbers"
|
||||
description: "Use Japanese-style numbers instead of standard numerals"
|
||||
prefField: barKey + ".modules.workspaces.showJapaneseNumbers"
|
||||
enabled: !barKey.modules.workspaces.showAppIcons
|
||||
opacity: !barKey.modules.workspaces.showAppIcons ? 1 : 0.8
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Status Icons"
|
||||
font.pixelSize: Metrics.fontSize(18)
|
||||
font.bold: true
|
||||
}
|
||||
StyledSwitchOption {
|
||||
title: "Enabled"
|
||||
description: "Show status icons module (wifi, bluetooth)"
|
||||
prefField: barKey + ".modules.statusIcons.enabled"
|
||||
}
|
||||
StyledSwitchOption {
|
||||
title: "Show Wifi Status"
|
||||
description: "Display wifi connection status and signal strength"
|
||||
prefField: barKey + ".modules.statusIcons.networkStatusEnabled"
|
||||
}
|
||||
StyledSwitchOption {
|
||||
title: "Show Bluetooth Status"
|
||||
description: "Display bluetooth connection status"
|
||||
prefField: barKey + ".modules.statusIcons.bluetoothStatusEnabled"
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "System Stats"
|
||||
font.pixelSize: Metrics.fontSize(18)
|
||||
font.bold: true
|
||||
}
|
||||
StyledSwitchOption {
|
||||
title: "Enabled"
|
||||
description: "Show system resource monitoring module"
|
||||
prefField: barKey + ".modules.systemUsage.enabled"
|
||||
}
|
||||
StyledSwitchOption {
|
||||
title: "Show Cpu Usage Stats"
|
||||
description: "Display CPU usage percentage and load"
|
||||
prefField: barKey + ".modules.systemUsage.cpuStatsEnabled"
|
||||
}
|
||||
StyledSwitchOption {
|
||||
title: "Show Memory Usage Stats"
|
||||
description: "Display RAM usage and available memory"
|
||||
prefField: barKey + ".modules.systemUsage.memoryStatsEnabled"
|
||||
}
|
||||
StyledSwitchOption {
|
||||
title: "Show Cpu Temperature Stats"
|
||||
description: "Display CPU temperature readings"
|
||||
prefField: barKey + ".modules.systemUsage.tempStatsEnabled"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.modules.functions
|
||||
import qs.services
|
||||
import Quickshell.Bluetooth as QsBluetooth
|
||||
|
||||
ContentMenu {
|
||||
title: "Bluetooth"
|
||||
description: "Manage Bluetooth devices and connections."
|
||||
|
||||
ContentCard {
|
||||
ContentRowCard {
|
||||
cardSpacing: Metrics.spacing(0)
|
||||
verticalPadding: Bluetooth.defaultAdapter.enabled ? Metrics.padding(10) : Metrics.padding(0)
|
||||
cardMargin: Metrics.margin(0)
|
||||
|
||||
StyledText {
|
||||
text: powerSwitch.checked ? "Power: On" : "Power: Off"
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
StyledSwitch {
|
||||
id: powerSwitch
|
||||
checked: Bluetooth.defaultAdapter?.enabled
|
||||
onToggled: Bluetooth.defaultAdapter.enabled = checked
|
||||
}
|
||||
}
|
||||
|
||||
ContentRowCard {
|
||||
visible: Bluetooth.defaultAdapter.enabled
|
||||
cardSpacing: Metrics.spacing(0)
|
||||
verticalPadding: Metrics.padding(10)
|
||||
cardMargin: Metrics.margin(0)
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Metrics.spacing(2)
|
||||
|
||||
StyledText {
|
||||
text: "Discoverable"
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Allow other devices to find this computer."
|
||||
font.pixelSize: Metrics.fontSize(12)
|
||||
color: ColorUtils.transparentize(Appearance.m3colors.m3onSurface, 0.6)
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
StyledSwitch {
|
||||
checked: Bluetooth.defaultAdapter?.discoverable
|
||||
onToggled: Bluetooth.defaultAdapter.discoverable = checked
|
||||
}
|
||||
}
|
||||
|
||||
ContentRowCard {
|
||||
visible: Bluetooth.defaultAdapter.enabled
|
||||
cardSpacing: Metrics.spacing(0)
|
||||
verticalPadding: Metrics.padding(0)
|
||||
cardMargin: Metrics.margin(0)
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Metrics.spacing(2)
|
||||
|
||||
StyledText {
|
||||
text: "Scanning"
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Search for nearby Bluetooth devices."
|
||||
font.pixelSize: Metrics.fontSize(12)
|
||||
color: ColorUtils.transparentize(Appearance.m3colors.m3onSurface, 0.6)
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
StyledSwitch {
|
||||
checked: Bluetooth.defaultAdapter?.discovering
|
||||
onToggled: Bluetooth.defaultAdapter.discovering = checked
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
visible: connectedDevices.count > 0
|
||||
|
||||
StyledText {
|
||||
text: "Connected Devices"
|
||||
font.pixelSize: Metrics.fontSize(18)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: connectedDevices
|
||||
model: Bluetooth.devices.filter(d => d.connected)
|
||||
|
||||
delegate: BluetoothDeviceCard {
|
||||
device: modelData
|
||||
statusText: modelData.batteryAvailable
|
||||
? "Connected, " + Math.floor(modelData.battery * 100) + "% left"
|
||||
: "Connected"
|
||||
showDisconnect: true
|
||||
showRemove: true
|
||||
usePrimary: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
visible: Bluetooth.defaultAdapter?.enabled
|
||||
|
||||
StyledText {
|
||||
text: "Paired Devices"
|
||||
font.pixelSize: Metrics.fontSize(18)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Item {
|
||||
visible: pairedDevices.count === 0
|
||||
width: parent.width
|
||||
height: Metrics.spacing(40)
|
||||
|
||||
StyledText {
|
||||
anchors.left: parent.left
|
||||
text: "No paired devices"
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
color: ColorUtils.transparentize(Appearance.m3colors.m3onSurface, 0.6)
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: pairedDevices
|
||||
model: Bluetooth.devices.filter(d => !d.connected && d.paired)
|
||||
|
||||
delegate: BluetoothDeviceCard {
|
||||
device: modelData
|
||||
statusText: "Not connected"
|
||||
showConnect: true
|
||||
showRemove: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
visible: Bluetooth.defaultAdapter?.enabled
|
||||
|
||||
StyledText {
|
||||
text: "Available Devices"
|
||||
font.pixelSize: Metrics.fontSize(18)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Item {
|
||||
visible: discoveredDevices.count === 0 && !Bluetooth.defaultAdapter.discovering
|
||||
width: parent.width
|
||||
height: Metrics.spacing(40)
|
||||
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: "No new devices found"
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
color: ColorUtils.transparentize(Appearance.m3colors.m3onSurface, 0.6)
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: discoveredDevices
|
||||
model: Bluetooth.devices.filter(d => !d.paired && !d.connected)
|
||||
|
||||
delegate: BluetoothDeviceCard {
|
||||
device: modelData
|
||||
statusText: "Discovered"
|
||||
showConnect: true
|
||||
showPair: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.modules.components
|
||||
import qs.config
|
||||
import qs.modules.functions
|
||||
import Quickshell.Bluetooth as QsBluetooth
|
||||
|
||||
ContentRowCard {
|
||||
id: deviceRow
|
||||
property var device
|
||||
property string statusText: ""
|
||||
property bool usePrimary: false
|
||||
property bool showConnect: false
|
||||
property bool showDisconnect: false
|
||||
property bool showPair: false
|
||||
property bool showRemove: false
|
||||
|
||||
cardMargin: Metrics.margin(0)
|
||||
cardSpacing: Metrics.spacing(10)
|
||||
verticalPadding: Metrics.padding(0)
|
||||
opacity: device.state === QsBluetooth.BluetoothDeviceState.Connecting ||
|
||||
device.state === QsBluetooth.BluetoothDeviceState.Disconnecting ? 0.6 : 1
|
||||
|
||||
function mapBluetoothIcon(dbusIcon, name) {
|
||||
console.log(dbusIcon, " / ", name)
|
||||
const iconMap = {
|
||||
"audio-headset": "headset",
|
||||
"audio-headphones": "headphones",
|
||||
"input-keyboard": "keyboard",
|
||||
"input-mouse": "mouse",
|
||||
"input-gaming": "sports_esports",
|
||||
"phone": "phone_android",
|
||||
"computer": "computer",
|
||||
"printer": "print",
|
||||
"camera": "photo_camera",
|
||||
"unknown": "bluetooth"
|
||||
}
|
||||
return iconMap[dbusIcon] || "bluetooth"
|
||||
}
|
||||
|
||||
MaterialSymbol {
|
||||
icon: mapBluetoothIcon(device.icon, device.name)
|
||||
font.pixelSize: Metrics.fontSize(32)
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
spacing: Metrics.spacing(0)
|
||||
|
||||
StyledText {
|
||||
text: device.name || device.address
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: statusText
|
||||
font.pixelSize: Metrics.fontSize(12)
|
||||
color: usePrimary
|
||||
? Appearance.m3colors.m3primary
|
||||
: ColorUtils.transparentize(Appearance.m3colors.m3onSurface, 0.6)
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
StyledButton {
|
||||
visible: showConnect
|
||||
icon: "link"
|
||||
onClicked: device.connect()
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
visible: showDisconnect
|
||||
icon: "link_off"
|
||||
onClicked: device.disconnect()
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
visible: showPair
|
||||
icon: "add"
|
||||
onClicked: device.pair()
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
visible: showRemove
|
||||
icon: "delete"
|
||||
onClicked: Bluetooth.removeDevice(device)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
|
||||
ContentMenu {
|
||||
title: "Launcher"
|
||||
description: "Adjust launcher's settings."
|
||||
|
||||
ContentCard {
|
||||
StyledText {
|
||||
text: "Filters & Search"
|
||||
font.pixelSize: Metrics.fontSize(20)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
StyledSwitchOption {
|
||||
title: "Fuzzy Search"
|
||||
description: "Enable or disable fuzzy search."
|
||||
prefField: "launcher.fuzzySearchEnabled"
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: webEngineSelector
|
||||
|
||||
property string title: "Web Search Engine"
|
||||
property string description: "Choose the web search engine for web searches."
|
||||
property string prefField: ''
|
||||
|
||||
ColumnLayout {
|
||||
StyledText {
|
||||
text: webEngineSelector.title
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: webEngineSelector.description
|
||||
font.pixelSize: Metrics.fontSize(12)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
StyledDropDown {
|
||||
label: "Engine"
|
||||
model: ["Google", "Brave", "DuckDuckGo", "Bing"]
|
||||
// Set the initial index based on the lowercase value in Config
|
||||
currentIndex: {
|
||||
switch (Config.runtime.launcher.webSearchEngine.toLowerCase()) {
|
||||
case "google":
|
||||
return 0;
|
||||
case "brave":
|
||||
return 1;
|
||||
case "duckduckgo":
|
||||
return 2;
|
||||
case "bing":
|
||||
return 3;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
onSelectedIndexChanged: (index) => {
|
||||
// Update Config with lowercase version of selected model
|
||||
Config.updateKey("launcher.webSearchEngine", model[index].toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
|
||||
ContentMenu {
|
||||
title: "Miscellaneous"
|
||||
description: "Configure misc settings."
|
||||
|
||||
ContentCard {
|
||||
StyledText {
|
||||
text: "Versions"
|
||||
font.pixelSize: Metrics.fontSize(20)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: releaseChannelSelector
|
||||
|
||||
property string title: "Release Channel"
|
||||
property string description: "Choose the release channel for updates."
|
||||
property string prefField: ''
|
||||
|
||||
ColumnLayout {
|
||||
StyledText {
|
||||
text: releaseChannelSelector.title
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: releaseChannelSelector.description
|
||||
font.pixelSize: Metrics.fontSize(12)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
StyledDropDown {
|
||||
label: "Type"
|
||||
model: ["Stable", "Edge (indev)"]
|
||||
currentIndex: Config.runtime.shell.releaseChannel === "edge" ? 1 : 0
|
||||
onSelectedIndexChanged: (index) => {
|
||||
Config.updateKey("shell.releaseChannel", index === 1 ? "edge" : "stable");
|
||||
UpdateNotifier.notified = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
StyledText {
|
||||
text: "Intelligence"
|
||||
font.pixelSize: Metrics.fontSize(20)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
StyledSwitchOption {
|
||||
title: "Enabled"
|
||||
description: "Enable or disable intelligence."
|
||||
prefField: "misc.intelligence.enabled"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
StyledText {
|
||||
text: "Intelligence Bearer/API"
|
||||
font.pixelSize: Metrics.fontSize(20)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
StyledTextField {
|
||||
id: apiKeyTextField
|
||||
|
||||
clip: true
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
placeholderText: Config.runtime.misc.intelligence.apiKey !== "" ? Config.runtime.misc.intelligence.apiKey : "Bearer Key"
|
||||
Layout.fillWidth: true
|
||||
Keys.onPressed: (event) => {
|
||||
if (event.key === Qt.Key_S && (event.modifiers & Qt.ControlModifier)) {
|
||||
event.accepted = true;
|
||||
Config.updateKey("misc.intelligence.apiKey", apiKeyTextField.text);
|
||||
Quickshell.execDetached(["notify-send", "Saved Bearer/API Key"])
|
||||
}
|
||||
}
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 20
|
||||
}
|
||||
|
||||
InfoCard {
|
||||
title: "How to save the api key"
|
||||
description: "In order to save the api key press Ctrl+S and it will save the api key to the config."
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
import qs.modules.functions
|
||||
|
||||
Item {
|
||||
id: networkRow
|
||||
property var connection
|
||||
property bool isActive: false
|
||||
property bool showConnect: false
|
||||
property bool showDisconnect: false
|
||||
property bool showPasswordField: false
|
||||
property string password: ""
|
||||
|
||||
width: parent.width
|
||||
implicitHeight: mainLayout.implicitHeight
|
||||
|
||||
function signalIcon(strength, secure) {
|
||||
if (!connection) return "network_wifi";
|
||||
if (connection.type === "ethernet") return "settings_ethernet";
|
||||
if (strength >= 75) return "network_wifi";
|
||||
if (strength >= 50) return "network_wifi_3_bar";
|
||||
if (strength >= 25) return "network_wifi_2_bar";
|
||||
if (strength > 0) return "network_wifi_1_bar";
|
||||
return "network_wifi_1_bar";
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: mainLayout
|
||||
anchors.fill: parent
|
||||
spacing: Metrics.spacing(10)
|
||||
|
||||
RowLayout {
|
||||
spacing: Metrics.spacing(10)
|
||||
|
||||
// Signal icon with lock overlay
|
||||
Item {
|
||||
width: Metrics.spacing(32)
|
||||
height: Metrics.spacing(32)
|
||||
|
||||
MaterialSymbol {
|
||||
anchors.fill: parent
|
||||
icon: connection ? signalIcon(connection.strength, connection.isSecure) : "network_wifi"
|
||||
font.pixelSize: Metrics.fontSize(32)
|
||||
}
|
||||
|
||||
// Lock overlay (anchors are safe because Item is not layout-managed)
|
||||
MaterialSymbol {
|
||||
icon: "lock"
|
||||
visible: connection && connection.type === "wifi" && connection.isSecure
|
||||
font.pixelSize: Metrics.fontSize(12)
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
spacing: Metrics.spacing(0)
|
||||
|
||||
StyledText {
|
||||
text: connection ? connection.name : ""
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: connection ? (
|
||||
isActive ? "Connected" :
|
||||
connection.type === "ethernet" ? connection.device || "Ethernet" :
|
||||
connection.isSecure ? "Secured" : "Open"
|
||||
) : ""
|
||||
font.pixelSize: Metrics.fontSize(12)
|
||||
color: isActive
|
||||
? Appearance.m3colors.m3primary
|
||||
: ColorUtils.transparentize(Appearance.m3colors.m3onSurface, 0.4)
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
StyledButton {
|
||||
visible: showConnect && !showPasswordField
|
||||
icon: "link"
|
||||
onClicked: {
|
||||
if (!connection) return;
|
||||
if (connection.type === "ethernet") Network.connect(connection, "")
|
||||
else if (connection.isSecure) showPasswordField = true
|
||||
else Network.connect(connection, "")
|
||||
}
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
visible: showDisconnect && !showPasswordField
|
||||
icon: "link_off"
|
||||
onClicked: Network.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
// Password row
|
||||
RowLayout {
|
||||
visible: showPasswordField && connection && connection.type === "wifi"
|
||||
spacing: Metrics.spacing(10)
|
||||
|
||||
StyledTextField {
|
||||
padding: Metrics.padding(10)
|
||||
Layout.fillWidth: true
|
||||
placeholderText: "Enter password"
|
||||
echoMode: parent.showPassword ? TextInput.Normal : TextInput.Password
|
||||
onTextChanged: networkRow.password = text
|
||||
onAccepted: {
|
||||
if (!connection) return;
|
||||
Network.connect(connection, networkRow.password)
|
||||
showPasswordField = false
|
||||
}
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
property bool showPassword: false
|
||||
icon: parent.showPassword ? "visibility" : "visibility_off"
|
||||
onClicked: parent.showPassword = !parent.showPassword
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
icon: "link"
|
||||
onClicked: {
|
||||
if (!connection) return;
|
||||
Network.connect(connection, networkRow.password)
|
||||
showPasswordField = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.modules.functions
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
|
||||
ContentMenu {
|
||||
title: "Network"
|
||||
description: "Manage network connections."
|
||||
|
||||
ContentCard {
|
||||
ContentRowCard {
|
||||
cardSpacing: Metrics.spacing(0)
|
||||
verticalPadding: Network.wifiEnabled ? Metrics.padding(10) : Metrics.padding(0)
|
||||
cardMargin: Metrics.margin(0)
|
||||
|
||||
StyledText {
|
||||
text: powerSwitch.checked ? "Wi-Fi: On" : "Wi-Fi: Off"
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
StyledSwitch {
|
||||
id: powerSwitch
|
||||
checked: Network.wifiEnabled
|
||||
onToggled: Network.enableWifi(checked)
|
||||
}
|
||||
}
|
||||
|
||||
ContentRowCard {
|
||||
visible: Network.wifiEnabled
|
||||
cardSpacing: Metrics.spacing(0)
|
||||
verticalPadding: Metrics.padding(10)
|
||||
cardMargin: Metrics.margin(0)
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Metrics.spacing(2)
|
||||
|
||||
StyledText {
|
||||
text: "Scanning"
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Search for nearby Wi-Fi networks."
|
||||
font.pixelSize: Metrics.fontSize(12)
|
||||
color: ColorUtils.transparentize(
|
||||
Appearance.m3colors.m3onSurface, 0.4
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
StyledSwitch {
|
||||
checked: Network.scanning
|
||||
onToggled: {
|
||||
if (checked)
|
||||
Network.rescan()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
InfoCard {
|
||||
visible: Network.message !== "" && Network.message !== "ok"
|
||||
icon: "error"
|
||||
backgroundColor: Appearance.m3colors.m3error
|
||||
contentColor: Appearance.m3colors.m3onError
|
||||
title: "Failed to connect to " + Network.lastNetworkAttempt
|
||||
description: Network.message
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
visible: Network.active !== null
|
||||
|
||||
StyledText {
|
||||
text: "Active Connection"
|
||||
font.pixelSize: Metrics.fontSize(18)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
NetworkCard {
|
||||
connection: Network.active
|
||||
isActive: true
|
||||
showDisconnect: Network.active?.type === "wifi"
|
||||
}
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
visible: Network.connections.filter(c => c.type === "ethernet").length > 0
|
||||
|
||||
StyledText {
|
||||
text: "Ethernet"
|
||||
font.pixelSize: Metrics.fontSize(18)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: Network.connections.filter(c => c.type === "ethernet" && !c.active)
|
||||
delegate: NetworkCard {
|
||||
connection: modelData
|
||||
showConnect: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
visible: Network.wifiEnabled
|
||||
|
||||
StyledText {
|
||||
text: "Available Wi-Fi Networks"
|
||||
font.pixelSize: Metrics.fontSize(18)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Item {
|
||||
visible: Network.connections.filter(c => c.type === "wifi").length === 0 && !Network.scanning
|
||||
width: parent.width
|
||||
height: Metrics.spacing(40)
|
||||
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
text: "No networks found"
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
color: ColorUtils.transparentize(Appearance.m3colors.m3onSurface, 0.4)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: Network.connections.filter(c => c.type === "wifi" && !c.active)
|
||||
delegate: NetworkCard {
|
||||
connection: modelData
|
||||
showConnect: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
visible: Network.savedNetworks.length > 0
|
||||
StyledText {
|
||||
text: "Remembered Networks"
|
||||
font.pixelSize: Metrics.fontSize(18)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Item {
|
||||
visible: Network.savedNetworks.length === 0
|
||||
width: parent.width
|
||||
height: Metrics.spacing(40)
|
||||
StyledText {
|
||||
anchors.left: parent.left
|
||||
text: "No remembered networks"
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
color: Appearance.colors.colSubtext
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: Network.connections.filter(c => c.type === "wifi" && c.saved && !c.active)
|
||||
delegate: NetworkCard {
|
||||
connection: modelData
|
||||
showConnect: false
|
||||
showDisconnect: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
import qs.modules.functions
|
||||
|
||||
ContentMenu {
|
||||
title: "Notifications & Overlays"
|
||||
description: "Adjust notification and overlay settings."
|
||||
|
||||
function indexFromPosition(pos, model) {
|
||||
pos = pos.toLowerCase()
|
||||
for (let i = 0; i < model.length; i++) {
|
||||
if (model[i].toLowerCase().replace(" ", "-") === pos)
|
||||
return i
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
|
||||
StyledText {
|
||||
text: "Notifications"
|
||||
font.pixelSize: Metrics.fontSize(20)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
StyledSwitchOption {
|
||||
title: "Enabled"
|
||||
description: "Enable or disable built-in notification daemon."
|
||||
prefField: "notifications.enabled"
|
||||
}
|
||||
|
||||
StyledSwitchOption {
|
||||
title: "Do not disturb enabled"
|
||||
description: "Enable or disable dnd."
|
||||
prefField: "notifications.doNotDisturb"
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
|
||||
ColumnLayout {
|
||||
StyledText {
|
||||
text: "Notification Position"
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Select where notification will be shown."
|
||||
font.pixelSize: Metrics.fontSize(12)
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
StyledDropDown {
|
||||
id: notificationDropdown
|
||||
label: "Position"
|
||||
|
||||
property var positions: ["Top Left", "Top Right", "Top"]
|
||||
|
||||
model: positions
|
||||
|
||||
currentIndex:
|
||||
indexFromPosition(
|
||||
Config.runtime.notifications.position,
|
||||
positions
|
||||
)
|
||||
|
||||
onSelectedIndexChanged: function(index) {
|
||||
Config.updateKey(
|
||||
"notifications.position",
|
||||
positions[index].toLowerCase().replace(" ", "-")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
|
||||
ColumnLayout {
|
||||
StyledText {
|
||||
text: "Test Notifications"
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Run a test notification."
|
||||
font.pixelSize: Metrics.fontSize(12)
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
StyledButton {
|
||||
text: "Test"
|
||||
icon: "chat"
|
||||
|
||||
onClicked:
|
||||
Quickshell.execDetached([
|
||||
"notify-send",
|
||||
"Quickshell",
|
||||
"This is a test notification"
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
|
||||
StyledText {
|
||||
text: "Overlays / OSDs"
|
||||
font.pixelSize: Metrics.fontSize(20)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
StyledSwitchOption {
|
||||
title: "Enabled"
|
||||
description: "Enable or disable built-in osd daemon."
|
||||
prefField: "overlays.enabled"
|
||||
}
|
||||
|
||||
StyledSwitchOption {
|
||||
title: "Volume OSD enabled"
|
||||
description: "Enable or disable volume osd."
|
||||
prefField: "overlays.volumeOverlayEnabled"
|
||||
}
|
||||
|
||||
StyledSwitchOption {
|
||||
title: "Brightness OSD enabled"
|
||||
description: "Enable or disable brightness osd."
|
||||
prefField: "overlays.brightnessOverlayEnabled"
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
|
||||
ColumnLayout {
|
||||
StyledText {
|
||||
text: "Brightness OSD Position"
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Choose where brightness OSD is shown."
|
||||
font.pixelSize: Metrics.fontSize(12)
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
StyledDropDown {
|
||||
|
||||
property var positions:
|
||||
["Top Left","Top Right","Bottom Left","Bottom Right","Top","Bottom"]
|
||||
|
||||
model: positions
|
||||
|
||||
currentIndex:
|
||||
indexFromPosition(
|
||||
Config.runtime.overlays.brightnessOverlayPosition,
|
||||
positions
|
||||
)
|
||||
|
||||
onSelectedIndexChanged: function(index) {
|
||||
Config.updateKey(
|
||||
"overlays.brightnessOverlayPosition",
|
||||
positions[index].toLowerCase().replace(" ", "-")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
|
||||
ColumnLayout {
|
||||
StyledText {
|
||||
text: "Volume OSD Position"
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Choose where volume OSD is shown."
|
||||
font.pixelSize: Metrics.fontSize(12)
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
StyledDropDown {
|
||||
|
||||
property var positions:
|
||||
["Top Left","Top Right","Bottom Left","Bottom Right","Top","Bottom"]
|
||||
|
||||
model: positions
|
||||
|
||||
currentIndex:
|
||||
indexFromPosition(
|
||||
Config.runtime.overlays.volumeOverlayPosition,
|
||||
positions
|
||||
)
|
||||
|
||||
onSelectedIndexChanged: function(index) {
|
||||
Config.updateKey(
|
||||
"overlays.volumeOverlayPosition",
|
||||
positions[index].toLowerCase().replace(" ", "-")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.plugins
|
||||
|
||||
ContentMenu {
|
||||
title: "Plugins"
|
||||
description: "Modify and Customize Installed Plugins."
|
||||
|
||||
ContentCard {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: implicitHeight
|
||||
color: "transparent"
|
||||
|
||||
GridLayout {
|
||||
id: grid
|
||||
columns: 1
|
||||
Layout.fillWidth: true
|
||||
columnSpacing: Metrics.spacing(16)
|
||||
rowSpacing: Metrics.spacing(16)
|
||||
|
||||
StyledText {
|
||||
text: "Plugins not found!"
|
||||
font.pixelSize: Metrics.fontSize(20)
|
||||
font.bold: true
|
||||
visible: PluginLoader.plugins.length === 0
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: PluginLoader.plugins
|
||||
|
||||
delegate: ContentCard {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Loader {
|
||||
Layout.fillWidth: true
|
||||
asynchronous: true
|
||||
source: Qt.resolvedUrl(
|
||||
Directories.shellConfig + "/plugins/" + modelData + "/Settings.qml"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Window
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import qs.config
|
||||
import qs.modules.functions
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
|
||||
Scope {
|
||||
property var settingsWindow: null
|
||||
|
||||
IpcHandler {
|
||||
function open(menu: string) {
|
||||
Globals.states.settingsOpen = true;
|
||||
|
||||
if (menu !== "" && settingsWindow !== null) {
|
||||
for (var i = 0; i < settingsWindow.menuModel.length; i++) {
|
||||
var item = settingsWindow.menuModel[i];
|
||||
if (!item.header && item.label.toLowerCase() === menu.toLowerCase()) {
|
||||
settingsWindow.selectedIndex = item.page;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
target: "settings"
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
active: Globals.states.settingsOpen
|
||||
|
||||
Window {
|
||||
id: root
|
||||
width: 1280
|
||||
height: 720
|
||||
visible: true
|
||||
title: "Nucleus - Settings"
|
||||
color: Appearance.m3colors.m3background
|
||||
onClosing: Globals.states.settingsOpen = false
|
||||
Component.onCompleted: settingsWindow = root
|
||||
|
||||
property int selectedIndex: 0
|
||||
property bool sidebarCollapsed: false
|
||||
|
||||
property var menuModel: [
|
||||
{ "header": true, "label": "System" },
|
||||
{ "icon": "bluetooth", "label": "Bluetooth", "page": 0 },
|
||||
{ "icon": "network_wifi", "label": "Network", "page": 1 },
|
||||
{ "icon": "volume_up", "label": "Audio", "page": 2 },
|
||||
{ "icon": "instant_mix", "label": "Appearance", "page": 3 },
|
||||
|
||||
{ "header": true, "label": "Customization" },
|
||||
{ "icon": "toolbar", "label": "Bar", "page": 4 },
|
||||
{ "icon": "wallpaper", "label": "Wallpapers", "page": 5 },
|
||||
{ "icon": "apps", "label": "Launcher", "page": 6 },
|
||||
{ "icon": "chat", "label": "Notifications", "page": 7 },
|
||||
{ "icon": "extension", "label": "Plugins", "page": 8 },
|
||||
{ "icon": "apps", "label": "Store", "page": 9 },
|
||||
{ "icon": "build", "label": "Miscellaneous", "page": 10 },
|
||||
|
||||
{ "header": true, "label": "About" },
|
||||
{ "icon": "info", "label": "About", "page": 11 }
|
||||
]
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
Rectangle {
|
||||
id: sidebarBG
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: root.sidebarCollapsed ? 80 : 350
|
||||
color: Appearance.m3colors.m3surfaceContainerLow
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Metrics.margin(40)
|
||||
spacing: Metrics.spacing(5)
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: "Settings"
|
||||
font.family: "Outfit ExtraBold"
|
||||
font.pixelSize: Metrics.fontSize(28)
|
||||
visible: !root.sidebarCollapsed
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
Layout.preferredHeight: 40
|
||||
icon: root.sidebarCollapsed ? "chevron_right" : "chevron_left"
|
||||
secondary: true
|
||||
onClicked: root.sidebarCollapsed = !root.sidebarCollapsed
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: sidebarList
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
model: root.menuModel
|
||||
spacing: Metrics.spacing(5)
|
||||
clip: true
|
||||
|
||||
delegate: Item {
|
||||
width: sidebarList.width
|
||||
height: modelData.header ? (root.sidebarCollapsed ? 0 : 30) : 42
|
||||
visible: !modelData.header || !root.sidebarCollapsed
|
||||
|
||||
// header
|
||||
Item {
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
|
||||
StyledText {
|
||||
y: (parent.height - height) * 0.5
|
||||
x: 10
|
||||
text: modelData.label
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
font.bold: true
|
||||
opacity: modelData.header ? 1 : 0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: !modelData.header
|
||||
radius: Appearance.rounding.large
|
||||
color: root.selectedIndex === modelData.page
|
||||
? Appearance.m3colors.m3primary
|
||||
: "transparent"
|
||||
|
||||
RowLayout {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 10
|
||||
spacing: 10
|
||||
|
||||
MaterialSymbol {
|
||||
visible: !modelData.header
|
||||
icon: modelData.icon ? modelData.icon : ""
|
||||
iconSize: Metrics.iconSize(24)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.label
|
||||
visible: !root.sidebarCollapsed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: modelData.page !== undefined
|
||||
onClicked: {
|
||||
root.selectedIndex = modelData.page
|
||||
settingsStack.currentIndex = modelData.page
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation { duration: 180; easing.type: Easing.InOutCubic }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
StackLayout {
|
||||
id: settingsStack
|
||||
anchors.left: sidebarBG.right
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
currentIndex: root.selectedIndex
|
||||
|
||||
BluetoothConfig { Layout.fillWidth: true; Layout.fillHeight: true }
|
||||
NetworkConfig { Layout.fillWidth: true; Layout.fillHeight: true }
|
||||
AudioConfig { Layout.fillWidth: true; Layout.fillHeight: true }
|
||||
AppearanceConfig { Layout.fillWidth: true; Layout.fillHeight: true }
|
||||
BarConfig { Layout.fillWidth: true; Layout.fillHeight: true }
|
||||
WallpaperConfig { Layout.fillWidth: true; Layout.fillHeight: true }
|
||||
LauncherConfig { Layout.fillWidth: true; Layout.fillHeight: true }
|
||||
NotificationConfig { Layout.fillWidth: true; Layout.fillHeight: true }
|
||||
Plugins { Layout.fillWidth: true; Layout.fillHeight: true }
|
||||
Store { Layout.fillWidth: true; Layout.fillHeight: true }
|
||||
MiscConfig { Layout.fillWidth: true; Layout.fillHeight: true }
|
||||
About { Layout.fillWidth: true; Layout.fillHeight: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.plugins
|
||||
|
||||
ContentMenu {
|
||||
title: "Store"
|
||||
description: "Manage plugins and other stuff for the shell."
|
||||
|
||||
ContentCard {
|
||||
Layout.fillWidth: true
|
||||
|
||||
GridLayout {
|
||||
columns: 1
|
||||
Layout.fillWidth: true
|
||||
columnSpacing: Metrics.spacing(16)
|
||||
rowSpacing: Metrics.spacing(16)
|
||||
|
||||
Repeater {
|
||||
model: PluginParser.model
|
||||
|
||||
delegate: StyledRect {
|
||||
Layout.preferredHeight: 90
|
||||
Layout.fillWidth: true
|
||||
radius: Metrics.radius("small")
|
||||
color: Appearance.m3colors.m3surfaceContainer
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Metrics.margin("normal")
|
||||
spacing: Metrics.spacing(12)
|
||||
|
||||
Column {
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(2)
|
||||
|
||||
StyledText {
|
||||
font.pixelSize: Metrics.fontSize("large")
|
||||
text: name
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Metrics.spacing(6)
|
||||
|
||||
StyledText {
|
||||
font.pixelSize: Metrics.fontSize("small")
|
||||
text: author
|
||||
color: Appearance.colors.colSubtext
|
||||
}
|
||||
|
||||
StyledText {
|
||||
font.pixelSize: Metrics.fontSize("small")
|
||||
text: "| Requires Nucleus " + requires_nucleus
|
||||
color: Appearance.colors.colSubtext
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
font.pixelSize: Metrics.fontSize("normal")
|
||||
text: description
|
||||
color: Appearance.colors.colSubtext
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Metrics.spacing(8)
|
||||
|
||||
StyledButton {
|
||||
icon: "download"
|
||||
text: "Install"
|
||||
visible: !installed
|
||||
secondary: true
|
||||
Layout.preferredWidth: 140
|
||||
onClicked: PluginParser.install(id)
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
icon: "update"
|
||||
text: "Update"
|
||||
visible: installed
|
||||
secondary: true
|
||||
Layout.preferredWidth: 140
|
||||
onClicked: PluginParser.update(id)
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
icon: "delete"
|
||||
text: "Remove"
|
||||
visible: installed
|
||||
secondary: true
|
||||
Layout.preferredWidth: 140
|
||||
onClicked: PluginParser.uninstall(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import qs.config
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
|
||||
ContentMenu {
|
||||
property string displayName: root.screen?.name ?? ""
|
||||
property var intervalOptions: [{
|
||||
"value": 5,
|
||||
"label": "5 minutes"
|
||||
}, {
|
||||
"value": 15,
|
||||
"label": "15 minutes"
|
||||
}, {
|
||||
"value": 30,
|
||||
"label": "30 minutes"
|
||||
}, {
|
||||
"value": 60,
|
||||
"label": "1 hour"
|
||||
}, {
|
||||
"value": 120,
|
||||
"label": "2 hours"
|
||||
}, {
|
||||
"value": 360,
|
||||
"label": "6 hours"
|
||||
}]
|
||||
|
||||
function getIntervalIndex(minutes) {
|
||||
for (let i = 0; i < intervalOptions.length; i++) {
|
||||
if (intervalOptions[i].value === minutes)
|
||||
return i;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
title: "Wallpaper"
|
||||
description: "Manage your wallpapers"
|
||||
|
||||
ContentCard {
|
||||
ClippingRectangle {
|
||||
id: wpContainer
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
width: root.screen.width / 2
|
||||
height: width * root.screen.height / root.screen.width
|
||||
radius: Metrics.radius("unsharpenmore")
|
||||
color: Appearance.m3colors.m3surfaceContainer
|
||||
|
||||
StyledText {
|
||||
text: "Current Wallpaper:"
|
||||
font.pixelSize: Metrics.fontSize("big")
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
ClippingRectangle {
|
||||
id: wpPreview
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignCenter
|
||||
anchors.fill: parent
|
||||
radius: Metrics.radius("unsharpenmore")
|
||||
color: Appearance.m3colors.m3paddingContainer
|
||||
layer.enabled: true
|
||||
|
||||
StyledText {
|
||||
opacity: !Config.runtime.appearance.background.enabled ? 1 : 0
|
||||
font.pixelSize: Metrics.fontSize("title")
|
||||
text: "Wallpaper Manager Disabled"
|
||||
anchors.centerIn: parent
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
Anim { }
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
opacity: Config.runtime.appearance.background.enabled ? 1 : 0
|
||||
anchors.fill: parent
|
||||
source: previewImg + "?t=" + Date.now()
|
||||
property string previewImg: {
|
||||
const displays = Config.runtime.monitors
|
||||
const fallback = Config.runtime.appearance.background.defaultPath
|
||||
|
||||
if (!displays)
|
||||
return fallback
|
||||
|
||||
const monitor = displays?.[displayName]
|
||||
return monitor?.wallpaper ?? fallback
|
||||
}
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
cache: true
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: Config.runtime.appearance.animations.enabled
|
||||
Anim { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
icon: "wallpaper"
|
||||
text: "Change Wallpaper"
|
||||
Layout.fillWidth: true
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["nucleus", "ipc", "call", "background", "change"]);
|
||||
}
|
||||
}
|
||||
|
||||
StyledSwitchOption {
|
||||
title: "Enabled"
|
||||
description: "Enabled or disable built-in wallpaper daemon."
|
||||
prefField: "appearance.background.enabled"
|
||||
}
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
StyledText {
|
||||
text: "Parallax Effect"
|
||||
font.pixelSize: Metrics.fontSize("big")
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
StyledSwitchOption {
|
||||
title: "Enabled"
|
||||
description: "Enabled or disable wallpaper parallax effect."
|
||||
prefField: "appearance.background.parallax.enabled"
|
||||
}
|
||||
|
||||
StyledSwitchOption {
|
||||
title: "Enabled for Sidebar Left"
|
||||
description: "Show parralax effect when sidebarLeft is opened."
|
||||
prefField: "appearance.background.parallax.enableSidebarLeft"
|
||||
}
|
||||
|
||||
StyledSwitchOption {
|
||||
title: "Enabled for Sidebar Right"
|
||||
description: "Show parralax effect when sidebarRight is opened."
|
||||
prefField: "appearance.background.parallax.enableSidebarRight"
|
||||
}
|
||||
|
||||
NumberStepper {
|
||||
label: "Zoom Amount"
|
||||
description: "Adjust the zoom of the parallax effect."
|
||||
prefField: "appearance.background.parallax.zoom"
|
||||
step: 0.1
|
||||
minimum: 1.10
|
||||
maximum: 2
|
||||
}
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
StyledText {
|
||||
text: "Wallpaper Slideshow"
|
||||
font.pixelSize: Metrics.fontSize("big")
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
StyledSwitchOption {
|
||||
title: "Enable Slideshow"
|
||||
description: "Automatically rotate wallpapers from a folder."
|
||||
prefField: "appearance.background.slideshow.enabled"
|
||||
}
|
||||
|
||||
StyledSwitchOption {
|
||||
title: "Include Subfolders"
|
||||
description: "Also search for wallpapers in subfolders."
|
||||
prefField: "appearance.background.slideshow.includeSubfolders"
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(8)
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(12)
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(4)
|
||||
|
||||
StyledText {
|
||||
text: "Wallpaper Folder"
|
||||
font.pixelSize: Metrics.fontSize("normal")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: Config.runtime.appearance.background.slideshow.folder || "No folder selected"
|
||||
font.pixelSize: Metrics.fontSize("small")
|
||||
color: Appearance.m3colors.m3onSurfaceVariant
|
||||
elide: Text.ElideMiddle
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
icon: "folder_open"
|
||||
text: "Browse"
|
||||
onClicked: folderPickerProc.running = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: skipWallpaper
|
||||
|
||||
property string title: "Skip To Next Wallpaper"
|
||||
property string description: "Skip to the next wallpaper in the wallpaper directory."
|
||||
property string prefField: ''
|
||||
|
||||
ColumnLayout {
|
||||
StyledText {
|
||||
text: skipWallpaper.title
|
||||
font.pixelSize: Metrics.fontSize("normal")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: skipWallpaper.description
|
||||
font.pixelSize: Metrics.fontSize("small")
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
StyledButton {
|
||||
icon: "skip_next"
|
||||
text: "Skip Next"
|
||||
enabled: WallpaperSlideshow.wallpapers.length > 0
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["nucleus", "ipc", "call", "background", "next"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(12)
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(4)
|
||||
|
||||
StyledText {
|
||||
text: "Change Interval"
|
||||
font.pixelSize: Metrics.fontSize("normal")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "How often to change the wallpaper."
|
||||
font.pixelSize: Metrics.fontSize("small")
|
||||
color: Appearance.m3colors.m3onSurfaceVariant
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
StyledDropDown {
|
||||
label: "Interval"
|
||||
model: intervalOptions.map((opt) => {
|
||||
return opt.label;
|
||||
})
|
||||
currentIndex: getIntervalIndex(Config.runtime.appearance.background.slideshow.interval)
|
||||
onSelectedIndexChanged: (index) => {
|
||||
Config.updateKey("appearance.background.slideshow.interval", intervalOptions[index].value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: folderPickerProc
|
||||
|
||||
command: ["bash", Directories.scriptsPath + "/interface/selectfolder.sh", Config.runtime.appearance.background.slideshow.folder || Directories.pictures]
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const out = text.trim();
|
||||
if (out !== "null" && out.length > 0)
|
||||
Config.updateKey("appearance.background.slideshow.folder", out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component Anim: NumberAnimation {
|
||||
duration: Metrics.chronoDuration(400)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.animation.curves.standard
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user