Justering/expand/marginaler, hover-popup med peek, appikon
All checks were successful
check / check (push) Successful in 1m37s

- Ny layoutmodell: alignment (center/start/end längs kanten), expand
  (ö eller täck hela kanten) och tre marginaler — A från kanten,
  B/C i början/slutet av kanten. Funkar för alla fyra positionerna.
- Settings → Layout har en ritad illustration (cairo) som visar vad
  A/B/C betyder för vald position och uppdateras live.
- Fönsterlistan för grupper kan öppnas vid hover (group_popup) och
  stängs när pekaren lämnar den; hover på en rad kan peeka (alla andra
  fönster tonas ner via Wayfires alpha-plugin) eller fokusera fönstret
  (list_hover: peek/focus/none).
- Appikon (assets/waydock.svg) + .desktop-fil; installeras av
  install.sh och används av settings-fönstret.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-30 00:03:15 +02:00
parent c9cdf2edc3
commit 04d999642c
10 changed files with 490 additions and 32 deletions

10
assets/waydock.desktop Normal file
View File

@@ -0,0 +1,10 @@
[Desktop Entry]
Type=Application
Name=waydock
Comment=Dock för Wayfire — transparent, JSON-konfigurerad, per skärm
Exec=waydock
Icon=waydock
Terminal=false
Categories=Utility;
Keywords=dock;panel;wayfire;taskbar;
X-GNOME-Autostart-enabled=true

36
assets/waydock.svg Normal file
View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#2e2e33"/>
<stop offset="1" stop-color="#1c1c20"/>
</linearGradient>
<linearGradient id="bar" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#3a3a41" stop-opacity="0.95"/>
<stop offset="1" stop-color="#26262b" stop-opacity="0.95"/>
</linearGradient>
<filter id="soft" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-color="#000000" flood-opacity="0.45"/>
</filter>
</defs>
<!-- skärmen -->
<rect x="8" y="8" width="112" height="112" rx="24" fill="url(#bg)"/>
<rect x="8" y="8" width="112" height="112" rx="24" fill="none" stroke="#ffffff" stroke-opacity="0.08" stroke-width="2"/>
<!-- antydda fönster -->
<rect x="26" y="26" width="42" height="30" rx="6" fill="#ffffff" fill-opacity="0.10"/>
<rect x="58" y="38" width="44" height="32" rx="6" fill="#ffffff" fill-opacity="0.16"/>
<!-- dockan -->
<rect x="22" y="84" width="84" height="24" rx="12" fill="url(#bar)" filter="url(#soft)"/>
<rect x="22" y="84" width="84" height="24" rx="12" fill="none" stroke="#ffffff" stroke-opacity="0.12" stroke-width="1.5"/>
<!-- appar i dockan -->
<circle cx="40" cy="96" r="7" fill="#c9545d"/>
<circle cx="64" cy="96" r="7" fill="#5a8dd6"/>
<circle cx="88" cy="96" r="7" fill="#5fb878"/>
<!-- indikatorprick under aktiv app -->
<circle cx="40" cy="112" r="2.2" fill="#c9545d"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -23,7 +23,12 @@ Alla ändringar i filen slår igenom **live** — ingen omstart behövs.
- **Högerklick på tom yta**: Inställningar…, Ladda om config, Avsluta
- **Visa skrivbord-knappen** minimerar allt; klick igen återställer
- **Översikt-knappen** togglar Wayfires scale-vy (kräver ipc-pluginen)
- Grupp-ikoner får en siffra och accentprickar; hovra för fönsterlistan
- Grupp-ikoner får en siffra och accentprickar. Fönsterlistan öppnas
vid klick eller hover (`behavior.group_popup`); hovra en rad i listan
för peek (bara det fönstret syns, andra tonas ner — kräver Wayfires
alpha-plugin) eller fokus, enligt `behavior.list_hover`
- Marginalillustrationen i Inställningar → Layout visar vad A/B/C-måtten
styr för aktuell position
## Settings-GUI
@@ -35,8 +40,11 @@ JSON-filen och appliceras live. GUI:t och handredigering kan blandas fritt.
```jsonc
{
"position": "bottom", // bottom | top | left | right
"centered": true, // ö i mitten i stället för hela kanten
"margin": 8, // px från skärmkant (0 = kant i kant)
"alignment": "center", // center | start | end — var längs kanten ön ligger
"expand": false, // true = täck hela kanten (panelen fyller bredden/höjden)
"margin": 8, // A: px från kanten baren ligger mot
"margin_start": 0, // B: marginal i början av kanten (vänster/topp)
"margin_end": 0, // C: marginal i slutet av kanten (höger/botten)
"layer": "top", // top | overlay | bottom (always-on-top = overlay)
"avoid_windows": false, // true = reservera plats (exclusive zone)
@@ -61,7 +69,10 @@ JSON-filen och appliceras live. GUI:t och handredigering kan blandas fritt.
"scroll": "cycle_windows", // cycle_windows | none
"group_apps": true,
"tooltips": true,
"urgent_pulse": true
"urgent_pulse": true,
"group_popup": "click", // click | hover — hur fönsterlistan öppnas
"list_hover": "peek" // peek (visa bara det fönstret, tona ner andra)
// | focus (fokusera vid hover) | none
},
"context_menu": ["minimize", "maximize", "move_to_output",

View File

@@ -17,6 +17,13 @@ curl -fSL -o "$DEST/waydock" \
"$BASE/releases/download/latest/waydock-latest-linux-x64"
chmod +x "$DEST/waydock"
# ikon + .desktop-fil
mkdir -p "$HOME/.local/share/icons/hicolor/scalable/apps" "$HOME/.local/share/applications"
curl -fsSL -o "$HOME/.local/share/icons/hicolor/scalable/apps/waydock.svg" \
"$BASE/raw/branch/master/assets/waydock.svg" || true
curl -fsSL -o "$HOME/.local/share/applications/waydock.desktop" \
"$BASE/raw/branch/master/assets/waydock.desktop" || true
echo "Installerad: $DEST/waydock"
case ":$PATH:" in
*":$DEST:"*) ;;

View File

@@ -28,6 +28,34 @@ pub enum Layer {
Bottom,
}
/// Var längs kanten ön ligger (start = vänster/topp, end = höger/botten).
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum Alignment {
Center,
Start,
End,
}
/// Hur fönsterlistan för en grupperad app öppnas.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum GroupPopup {
Click,
Hover,
}
/// Vad som händer när man hovrar en rad i fönsterlistan.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum ListHover {
/// tona ner alla andra fönster så bara det hovrade syns tydligt
Peek,
/// fokusera/lyft fönstret direkt vid hover
Focus,
None,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum ClickAction {
@@ -151,6 +179,8 @@ pub struct Behavior {
pub group_apps: bool,
pub tooltips: bool,
pub urgent_pulse: bool,
pub group_popup: GroupPopup,
pub list_hover: ListHover,
}
impl Default for Behavior {
fn default() -> Self {
@@ -161,6 +191,8 @@ impl Default for Behavior {
group_apps: true,
tooltips: true,
urgent_pulse: true,
group_popup: GroupPopup::Click,
list_hover: ListHover::Peek,
}
}
}
@@ -268,8 +300,15 @@ impl Default for LogConfig {
#[serde(default)]
pub struct Config {
pub position: Position,
pub centered: bool,
pub alignment: Alignment,
/// false = ö som är så stor som innehållet; true = täck hela kanten
pub expand: bool,
/// avstånd från kanten baren ligger mot (px)
pub margin: i32,
/// marginal i början av kanten: vänster (topp/botten) resp. topp (vänster/höger)
pub margin_start: i32,
/// marginal i slutet av kanten: höger resp. botten
pub margin_end: i32,
pub layer: Layer,
pub avoid_windows: bool,
pub appearance: Appearance,
@@ -287,8 +326,11 @@ impl Default for Config {
fn default() -> Self {
Self {
position: Position::Bottom,
centered: true,
alignment: Alignment::Center,
expand: false,
margin: 8,
margin_start: 0,
margin_end: 0,
layer: Layer::Top,
avoid_windows: false,
appearance: Appearance::default(),

View File

@@ -16,8 +16,8 @@ use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell};
use log::{debug, info};
use crate::config::{
AutohideMode, ClickAction, Layer as CfgLayer, MiddleClick, Position, ScrollAction,
SharedConfig,
Alignment, AutohideMode, ClickAction, GroupPopup, Layer as CfgLayer, MiddleClick,
Position, ScrollAction, SharedConfig,
};
use crate::icons::{self, AppMeta};
use crate::menu;
@@ -45,7 +45,7 @@ pub struct Ctx {
pub struct Dock {
pub window: gtk::ApplicationWindow,
pub panel: gtk::Box,
pub items_box: gtk::Box,
pub revealer: gtk::Revealer,
pub connector: String,
/// pekaren är över dockan just nu
@@ -103,12 +103,28 @@ fn configure_layer(win: &gtk::ApplicationWindow, ctx: &Ctx, monitor: &gdk::Monit
win.set_keyboard_mode(KeyboardMode::None);
let edge = main_edge(&cfg.position);
let (side_a, side_b) = side_edges(&cfg.position);
let (side_start, side_end) = side_edges(&cfg.position);
win.set_anchor(edge, true);
win.set_margin(edge, cfg.margin);
if !cfg.centered {
win.set_anchor(side_a, true);
win.set_anchor(side_b, true);
if cfg.expand {
// täck hela kanten; start/slut-marginalerna drar in baren
win.set_anchor(side_start, true);
win.set_anchor(side_end, true);
win.set_margin(side_start, cfg.margin_start);
win.set_margin(side_end, cfg.margin_end);
} else {
// ö: förankra bara i vald ände (ingen sidoförankring = centrerad)
match cfg.alignment {
Alignment::Center => {}
Alignment::Start => {
win.set_anchor(side_start, true);
win.set_margin(side_start, cfg.margin_start);
}
Alignment::End => {
win.set_anchor(side_end, true);
win.set_margin(side_end, cfg.margin_end);
}
}
}
if cfg.avoid_windows && !cfg.autohide.enabled {
win.auto_exclusive_zone_enable();
@@ -158,8 +174,30 @@ fn build_dock(ctx: &Rc<Ctx>, monitor: &gdk::Monitor, connector: String) -> Rc<Do
let orient = orientation(&cfg.position);
let panel = gtk::Box::new(orient, 0);
panel.add_css_class("dock-panel");
panel.set_halign(gtk::Align::Center);
panel.set_valign(gtk::Align::Center);
let items_align = match cfg.alignment {
Alignment::Center => gtk::Align::Center,
Alignment::Start => gtk::Align::Start,
Alignment::End => gtk::Align::End,
};
let items_box = gtk::Box::new(orient, 0);
if cfg.expand {
// panelbakgrunden täcker kanten; ikonerna läggs enligt alignment
panel.set_halign(gtk::Align::Fill);
panel.set_valign(gtk::Align::Fill);
if orient == gtk::Orientation::Horizontal {
panel.set_hexpand(true);
items_box.set_hexpand(true);
items_box.set_halign(items_align);
} else {
panel.set_vexpand(true);
items_box.set_vexpand(true);
items_box.set_valign(items_align);
}
} else {
panel.set_halign(gtk::Align::Center);
panel.set_valign(gtk::Align::Center);
}
panel.append(&items_box);
// yttre marginal = plats för skuggan att rita
let outer = gtk::Box::new(orient, 0);
@@ -198,7 +236,7 @@ fn build_dock(ctx: &Rc<Ctx>, monitor: &gdk::Monitor, connector: String) -> Rc<Do
let dock = Rc::new(Dock {
window: win.clone(),
panel: panel.clone(),
items_box: items_box.clone(),
revealer,
connector,
hover: Cell::new(false),
@@ -481,13 +519,13 @@ pub fn refresh_items(ctx: &Rc<Ctx>) {
.unwrap_or_default();
for dock in ctx.docks.borrow().iter() {
while let Some(child) = dock.panel.first_child() {
dock.panel.remove(&child);
while let Some(child) = dock.items_box.first_child() {
dock.items_box.remove(&child);
}
add_special_buttons(ctx, dock);
for item in build_items(ctx, &dock.connector, &snapshot) {
let widget = build_item_widget(ctx, &dock.connector, Rc::new(item));
dock.panel.append(&widget);
dock.items_box.append(&widget);
}
}
}
@@ -509,7 +547,7 @@ fn add_special_buttons(ctx: &Rc<Ctx>, dock: &Rc<Dock>) {
if specials.overview && wayfire_ipc::available() {
let btn = special_button("view-grid-symbolic", "Översikt (scale)");
btn.connect_clicked(|_| wayfire_ipc::toggle_scale());
dock.panel.append(&btn);
dock.items_box.append(&btn);
}
if specials.show_desktop {
let btn = special_button("user-desktop-symbolic", "Visa skrivbordet");
@@ -519,7 +557,7 @@ fn add_special_buttons(ctx: &Rc<Ctx>, dock: &Rc<Dock>) {
service.toggle_show_desktop();
}
});
dock.panel.append(&btn);
dock.items_box.append(&btn);
}
}
@@ -610,6 +648,33 @@ fn build_item_widget(ctx: &Rc<Ctx>, connector: &str, item: Rc<Item>) -> gtk::But
}
btn.add_controller(click);
// hover-popup för grupperade appar (om valt i configen)
if item.windows.len() > 1
&& ctx.cfg.borrow().behavior.group_popup == GroupPopup::Hover
{
let hover_ctl = gtk::EventControllerMotion::new();
let ctx2 = ctx.clone();
let item2 = item.clone();
let btn2 = btn.clone();
let conn2 = connector.to_string();
hover_ctl.connect_enter(move |ctl, _, _| {
let ctx3 = ctx2.clone();
let item3 = item2.clone();
let btn3 = btn2.clone();
let conn3 = conn2.clone();
let ctl2 = ctl.clone();
glib::timeout_add_local_once(Duration::from_millis(350), move || {
if ctl2.contains_pointer() {
let live = live_item(&ctx3, &conn3, &item3);
if live.windows.len() > 1 {
menu::popup_group_list(&ctx3, &live, &btn3);
}
}
});
});
btn.add_controller(hover_ctl);
}
// skroll: växla mellan gruppens fönster
let scroll = gtk::EventControllerScroll::new(gtk::EventControllerScrollFlags::VERTICAL);
{
@@ -649,8 +714,11 @@ fn primary_action(ctx: &Rc<Ctx>, item: &Rc<Item>, anchor: &gtk::Button) {
}
}
_ => {
let click = ctx.cfg.borrow().behavior.click.clone();
if click == ClickAction::Cycle {
let (click, popup) = {
let b = &ctx.cfg.borrow().behavior;
(b.click.clone(), b.group_popup.clone())
};
if click == ClickAction::Cycle || popup == GroupPopup::Hover {
cycle_windows(ctx, item, true);
} else {
menu::popup_group_list(ctx, item, anchor);

View File

@@ -102,10 +102,13 @@ fn init_logging(cli: &Cli, cfg: &config::Config) {
/// Delar av configen som kräver att fönstren byggs om (inte bara CSS).
fn layout_signature(cfg: &config::Config) -> String {
format!(
"{:?}|{}|{}|{:?}|{}|{}|{}|{:?}",
"{:?}|{:?}|{}|{}|{}|{}|{:?}|{}|{}|{}|{:?}",
cfg.position,
cfg.centered,
cfg.alignment,
cfg.expand,
cfg.margin,
cfg.margin_start,
cfg.margin_end,
cfg.layer,
cfg.avoid_windows,
cfg.appearance.shadow.enabled,

View File

@@ -11,7 +11,7 @@ use gtk4::glib;
use gtk4::prelude::*;
use log::warn;
use crate::config::{self, MenuItem};
use crate::config::{self, GroupPopup, ListHover, MenuItem};
use crate::dock::{Ctx, Item};
use crate::icons;
use crate::settings;
@@ -232,12 +232,56 @@ pub fn popup_item_menu(ctx: &Rc<Ctx>, item: &Rc<Item>, anchor: &gtk::Button) {
popover.popup();
}
/// Vänsterklick på grupp med flera fönster: lista fönstren.
/// Tona ner alla andra fönster än det hovrade (peek). Returnerar id:n
/// som dimmats så de kan återställas.
fn peek_apply(hovered: &crate::toplevel::ToplevelInfo) -> Vec<i64> {
let Some(views) = wayfire_ipc::list_views() else {
return Vec::new();
};
let hovered_id = views
.iter()
.find(|v| {
v["app-id"].as_str() == Some(hovered.app_id.as_str())
&& v["title"].as_str() == Some(hovered.title.as_str())
})
.and_then(|v| v["id"].as_i64());
let mut dimmed = Vec::new();
for v in &views {
if v["role"].as_str() != Some("toplevel")
|| !v["mapped"].as_bool().unwrap_or(false)
|| v["minimized"].as_bool().unwrap_or(false)
{
continue;
}
let Some(id) = v["id"].as_i64() else { continue };
if Some(id) != hovered_id {
wayfire_ipc::set_view_alpha(id, 0.15);
dimmed.push(id);
}
}
dimmed
}
fn peek_restore(dimmed: &[i64]) {
for id in dimmed {
wayfire_ipc::set_view_alpha(*id, 1.0);
}
}
/// Vänsterklick (eller hover) på grupp med flera fönster: lista fönstren.
pub fn popup_group_list(ctx: &Rc<Ctx>, item: &Rc<Item>, anchor: &gtk::Button) {
let popover = gtk::Popover::new();
popover.add_css_class("waydock-menu");
let list = gtk::Box::new(gtk::Orientation::Vertical, 2);
let (list_hover, hover_mode) = {
let b = &ctx.cfg.borrow().behavior;
(b.list_hover.clone(), b.group_popup == GroupPopup::Hover)
};
let peek_possible = list_hover == ListHover::Peek && wayfire_ipc::available();
// fönster som tonats ner av peek — återställs vid radbyte/stängning
let dimmed: Rc<std::cell::RefCell<Vec<i64>>> = Rc::new(std::cell::RefCell::new(Vec::new()));
for w in &item.windows {
let row = gtk::Button::new();
row.add_css_class("flat");
@@ -250,16 +294,76 @@ pub fn popup_group_list(ctx: &Rc<Ctx>, item: &Rc<Item>, anchor: &gtk::Button) {
let ctx2 = ctx.clone();
let w2 = w.clone();
let pop2 = popover.clone();
let dimmed2 = dimmed.clone();
row.connect_clicked(move |_| {
peek_restore(&dimmed2.borrow());
dimmed2.borrow_mut().clear();
if let Some(s) = ctx2.service.borrow().as_ref() {
s.activate(&w2);
}
pop2.popdown();
});
// hover-effekt per rad: peek eller fokus
match list_hover {
ListHover::Peek if peek_possible => {
let mc = gtk::EventControllerMotion::new();
let w3 = w.clone();
let dimmed3 = dimmed.clone();
mc.connect_enter(move |_, _, _| {
peek_restore(&dimmed3.borrow());
*dimmed3.borrow_mut() = peek_apply(&w3);
});
let dimmed4 = dimmed.clone();
mc.connect_leave(move |_| {
peek_restore(&dimmed4.borrow());
dimmed4.borrow_mut().clear();
});
row.add_controller(mc);
}
ListHover::Focus => {
let mc = gtk::EventControllerMotion::new();
let ctx3 = ctx.clone();
let w3 = w.clone();
mc.connect_enter(move |_, _, _| {
if let Some(s) = ctx3.service.borrow().as_ref() {
s.activate(&w3);
}
});
row.add_controller(mc);
}
_ => {}
}
list.append(&row);
}
popover.set_child(Some(&list));
popover.set_parent(anchor);
// säkerhetsnät: återställ peek när popovern stängs
{
let dimmed5 = dimmed.clone();
popover.connect_closed(move |_| {
peek_restore(&dimmed5.borrow());
dimmed5.borrow_mut().clear();
});
}
// i hover-läge: stäng listan när pekaren lämnar den
if hover_mode {
let mc = gtk::EventControllerMotion::new();
let pop3 = popover.clone();
mc.connect_leave(move |ctl| {
let pop4 = pop3.clone();
let ctl2 = ctl.clone();
glib::timeout_add_local_once(std::time::Duration::from_millis(400), move || {
if !ctl2.contains_pointer() {
pop4.popdown();
}
});
});
popover.add_controller(mc);
}
autodispose(&popover);
popover.popup();
}

View File

@@ -14,14 +14,16 @@ use gtk4::prelude::*;
use log::warn;
use crate::config::{
self, AutohideMode, ClickAction, HotspotAction, IndicatorStyle, Layer, MenuItem,
MiddleClick, Position, ScrollAction,
self, Alignment, AutohideMode, ClickAction, GroupPopup, HotspotAction, IndicatorStyle,
Layer, ListHover, MenuItem, MiddleClick, Position, ScrollAction,
};
use crate::dock::Ctx;
thread_local! {
static OPEN_WINDOW: std::cell::RefCell<Option<glib::WeakRef<gtk::Window>>> =
const { std::cell::RefCell::new(None) };
static ILLUSTRATION: std::cell::RefCell<Option<glib::WeakRef<gtk::DrawingArea>>> =
const { std::cell::RefCell::new(None) };
}
/// Uppdatera config + applicera + spara (debouncat 300 ms).
@@ -30,6 +32,11 @@ fn commit(ctx: &Rc<Ctx>, mutate: impl FnOnce(&mut config::Config)) {
if let Some(cb) = ctx.on_config_changed.borrow().as_ref() {
cb();
}
ILLUSTRATION.with(|i| {
if let Some(da) = i.borrow().as_ref().and_then(|w| w.upgrade()) {
da.queue_draw();
}
});
let generation = ctx.save_generation.get() + 1;
ctx.save_generation.set(generation);
let ctx = ctx.clone();
@@ -253,6 +260,125 @@ fn build_appearance_page(ctx: &Rc<Ctx>) -> gtk::ScrolledWindow {
scroll
}
fn margin_illustration(ctx: &Rc<Ctx>) -> gtk::DrawingArea {
let da = gtk::DrawingArea::new();
da.set_content_width(280);
da.set_content_height(190);
da.set_halign(gtk::Align::Center);
let ctx2 = ctx.clone();
da.set_draw_func(move |_, cr, w, h| {
let cfg = ctx2.cfg.borrow();
let (w, h) = (w as f64, h as f64);
// "skärmen"
let (sx, sy, sw, sh) = (30.0, 22.0, w - 60.0, h - 60.0);
cr.set_source_rgba(0.16, 0.16, 0.18, 1.0);
cr.rectangle(sx, sy, sw, sh);
let _ = cr.fill();
cr.set_source_rgba(0.5, 0.5, 0.55, 1.0);
cr.set_line_width(1.5);
cr.rectangle(sx, sy, sw, sh);
let _ = cr.stroke();
// förhållandena skalade: marginalerna visas symboliskt (max ~18 px)
let clamp = |v: i32| 6.0 + (v.max(0) as f64).min(60.0) * 0.2;
let a = clamp(cfg.margin);
let b = clamp(cfg.margin_start);
let c = clamp(cfg.margin_end);
let thick = 12.0;
let horizontal = matches!(cfg.position, Position::Top | Position::Bottom);
// barens längd/placering längs kanten
let full = if horizontal { sw } else { sh };
let len = if cfg.expand {
full - b - c
} else {
(full * 0.45).max(40.0)
};
let along = if cfg.expand {
b
} else {
match cfg.alignment {
Alignment::Start => b,
Alignment::End => full - len - c,
Alignment::Center => (full - len) / 2.0,
}
};
// accentfärgad bar
let acc = &cfg.appearance.accent;
let hex = acc.trim_start_matches('#');
let (r, g, bl) = if hex.len() == 6 {
(
u8::from_str_radix(&hex[0..2], 16).unwrap_or(200) as f64 / 255.0,
u8::from_str_radix(&hex[2..4], 16).unwrap_or(85) as f64 / 255.0,
u8::from_str_radix(&hex[4..6], 16).unwrap_or(95) as f64 / 255.0,
)
} else {
(0.79, 0.33, 0.36)
};
cr.set_source_rgba(r, g, bl, 0.95);
let (bx, by, bw, bh) = match cfg.position {
Position::Bottom => (sx + along, sy + sh - a - thick, len, thick),
Position::Top => (sx + along, sy + a, len, thick),
Position::Left => (sx + a, sy + along, thick, len),
Position::Right => (sx + sw - a - thick, sy + along, thick, len),
};
cr.rectangle(bx, by, bw, bh);
let _ = cr.fill();
// måttpilar med etiketter
cr.set_source_rgba(0.85, 0.85, 0.88, 1.0);
cr.set_line_width(1.0);
cr.set_font_size(11.0);
let label = |x: f64, y: f64, text: &str| {
cr.move_to(x, y);
let _ = cr.show_text(text);
};
let line = |x1: f64, y1: f64, x2: f64, y2: f64| {
cr.move_to(x1, y1);
cr.line_to(x2, y2);
let _ = cr.stroke();
};
match cfg.position {
Position::Bottom => {
line(bx + bw / 2.0, by + thick, bx + bw / 2.0, sy + sh);
label(bx + bw / 2.0 + 3.0, sy + sh - 2.0, "A");
line(sx, by + thick / 2.0, bx, by + thick / 2.0);
label(sx + (bx - sx) / 2.0 - 3.0, by - 3.0, "B");
line(bx + bw, by + thick / 2.0, sx + sw, by + thick / 2.0);
label(bx + bw + (sx + sw - bx - bw) / 2.0 - 3.0, by - 3.0, "C");
}
Position::Top => {
line(bx + bw / 2.0, sy, bx + bw / 2.0, by);
label(bx + bw / 2.0 + 3.0, sy + 12.0, "A");
line(sx, by + thick / 2.0, bx, by + thick / 2.0);
label(sx + (bx - sx) / 2.0 - 3.0, by + thick + 12.0, "B");
line(bx + bw, by + thick / 2.0, sx + sw, by + thick / 2.0);
label(bx + bw + (sx + sw - bx - bw) / 2.0 - 3.0, by + thick + 12.0, "C");
}
Position::Left => {
line(sx, by + bh / 2.0, bx, by + bh / 2.0);
label(sx + 2.0, by + bh / 2.0 - 4.0, "A");
line(bx + thick / 2.0, sy, bx + thick / 2.0, by);
label(bx + thick + 3.0, sy + (by - sy) / 2.0, "B");
line(bx + thick / 2.0, by + bh, bx + thick / 2.0, sy + sh);
label(bx + thick + 3.0, by + bh + (sy + sh - by - bh) / 2.0, "C");
}
Position::Right => {
line(bx + thick, by + bh / 2.0, sx + sw, by + bh / 2.0);
label(sx + sw - 12.0, by + bh / 2.0 - 4.0, "A");
line(bx + thick / 2.0, sy, bx + thick / 2.0, by);
label(bx - 12.0, sy + (by - sy) / 2.0, "B");
line(bx + thick / 2.0, by + bh, bx + thick / 2.0, sy + sh);
label(bx - 12.0, by + bh + (sy + sh - by - bh) / 2.0, "C");
}
}
});
ILLUSTRATION.with(|i| *i.borrow_mut() = Some(da.downgrade()));
da
}
fn build_layout_page(ctx: &Rc<Ctx>) -> gtk::ScrolledWindow {
let (scroll, list) = page();
let cfg = ctx.cfg.borrow().clone();
@@ -272,8 +398,19 @@ fn build_layout_page(ctx: &Rc<Ctx>) -> gtk::ScrolledWindow {
_ => Position::Bottom,
};
}));
row(&list, "Centrerad ö", &switch(ctx, cfg.centered, |c, v| c.centered = v));
row(&list, "Marginal från kant", &spin(ctx, cfg.margin as f64, (0.0, 64.0), 1.0, |c, v| c.margin = v as i32));
let align_idx = match cfg.alignment {
Alignment::Center => 0,
Alignment::Start => 1,
Alignment::End => 2,
};
row(&list, "Justering längs kanten", &dropdown(ctx, &["Centrerad", "Start (vänster/topp)", "Slut (höger/botten)"], align_idx, |c, i| {
c.alignment = match i {
1 => Alignment::Start,
2 => Alignment::End,
_ => Alignment::Center,
};
}));
row(&list, "Täck hela kanten", &switch(ctx, cfg.expand, |c, v| c.expand = v));
let layer_idx = match cfg.layer {
Layer::Top => 0,
Layer::Overlay => 1,
@@ -288,6 +425,18 @@ fn build_layout_page(ctx: &Rc<Ctx>) -> gtk::ScrolledWindow {
}));
row(&list, "Reservera plats (undvik fönster)", &switch(ctx, cfg.avoid_windows, |c, v| c.avoid_windows = v));
heading(&list, "Marginaler");
list.append(&margin_illustration(ctx));
row(&list, "A — Avstånd från kanten", &spin(ctx, cfg.margin as f64, (0.0, 128.0), 1.0, |c, v| c.margin = v as i32));
row(&list, "B — Marginal start", &spin(ctx, cfg.margin_start as f64, (0.0, 512.0), 1.0, |c, v| c.margin_start = v as i32));
row(&list, "C — Marginal slut", &spin(ctx, cfg.margin_end as f64, (0.0, 512.0), 1.0, |c, v| c.margin_end = v as i32));
let note = gtk::Label::new(Some(
"A = avstånd från kanten baren ligger mot.\nB/C = marginal i början/slutet av kanten — indrag när baren\ntäcker kanten, annars avstånd från vald sida (vid Start/Slut).",
));
note.add_css_class("dim-label");
note.set_halign(gtk::Align::Start);
list.append(&note);
heading(&list, "Autohide");
row(&list, "Aktiverad", &switch(ctx, cfg.autohide.enabled, |c, v| c.autohide.enabled = v));
let mode_idx = match cfg.autohide.mode {
@@ -348,8 +497,27 @@ fn build_behavior_page(ctx: &Rc<Ctx>) -> gtk::ScrolledWindow {
c.behavior.scroll = if i == 1 { ScrollAction::None } else { ScrollAction::CycleWindows };
}));
heading(&list, "Övrigt");
heading(&list, "Gruppering & popup");
row(&list, "Gruppera appar", &switch(ctx, b.group_apps, |c, v| c.behavior.group_apps = v));
let popup_idx = match b.group_popup {
GroupPopup::Click => 0,
GroupPopup::Hover => 1,
};
row(&list, "Fönsterlistan öppnas", &dropdown(ctx, &["Vid klick", "Vid hover"], popup_idx, |c, i| {
c.behavior.group_popup = if i == 1 { GroupPopup::Hover } else { GroupPopup::Click };
}));
let lh_idx = match b.list_hover {
ListHover::Peek => 0,
ListHover::Focus => 1,
ListHover::None => 2,
};
row(&list, "Hover i fönsterlistan", &dropdown(ctx, &["Peek — visa bara det fönstret", "Fokusera fönstret", "Ingen effekt"], lh_idx, |c, i| {
c.behavior.list_hover = match i {
1 => ListHover::Focus,
2 => ListHover::None,
_ => ListHover::Peek,
};
}));
row(&list, "Tooltips", &switch(ctx, b.tooltips, |c, v| c.behavior.tooltips = v));
row(&list, "Urgency-puls", &switch(ctx, b.urgent_pulse, |c, v| c.behavior.urgent_pulse = v));
@@ -510,6 +678,7 @@ pub fn open(ctx: &Rc<Ctx>) {
let win = gtk::Window::new();
win.set_title(Some("waydock — inställningar"));
win.set_icon_name(Some("waydock"));
win.set_default_size(520, 620);
let notebook = gtk::Notebook::new();

View File

@@ -178,6 +178,14 @@ pub fn subscribe(on_event: std::rc::Rc<dyn Fn(&Value)>) -> Result<(), String> {
Ok(())
}
/// Sätt en vys genomskinlighet (kräver alpha-pluginen).
pub fn set_view_alpha(view_id: i64, alpha: f64) {
call_logged(
"wf/alpha/set-view-alpha",
json!({ "view-id": view_id, "alpha": alpha }),
);
}
/// Flytta en vy till en annan output (namn t.ex. "DP-1").
pub fn move_view_to_output(view_id: i64, output_name: &str) {
let Some(outputs) = list_outputs() else {