"""Appearance page — borders, focus ring, shadows, corner radius.""" from __future__ import annotations import gi gi.require_version("Gtk", "4.0") gi.require_version("Adw", "1") from gi.repository import Adw, Gdk, Gtk from nirimod.kdl_parser import KdlNode, find_or_create, set_child_arg, set_node_flag from nirimod.pages.base import BasePage from nirimod.window_effects import ( blur_effects_enabled, focused_window_blur_enabled, get_global_draw_border_with_background, get_global_corner_radius, get_global_window_opacity, global_window_blur_enabled, global_window_xray_enabled, set_focused_window_blur, set_global_draw_border_with_background, set_global_corner_radius, set_global_window_blur, set_global_window_opacity, set_global_window_xray, set_blur_effects_enabled, ) def _parse_color(color_str: str) -> Gdk.RGBA: rgba = Gdk.RGBA() if color_str and not rgba.parse(color_str): rgba.parse("#7fc8ff") return rgba class AppearancePage(BasePage): def build(self) -> Gtk.Widget: tb, _, _, content = self._make_toolbar_page("Appearance") self._content = content self._build_content() return tb def _build_content(self): content = self._content nodes = self._nodes layout = find_or_create(nodes, "layout") fr_node = layout.get_child("focus-ring") or KdlNode("focus-ring") fr_group = self._build_border_group("Focus Ring", "focus-ring", fr_node, layout) content.append(fr_group) b_node = layout.get_child("border") or KdlNode("border") b_group = self._build_border_group("Border", "border", b_node, layout) content.append(b_group) shadow_grp = Adw.PreferencesGroup(title="Shadow") shadow_node = layout.get_child("shadow") or KdlNode("shadow") shadow_on_row = Adw.SwitchRow(title="Enable Shadows") shadow_on_row.set_active(shadow_node.get_child("on") is not None) shadow_on_row.connect( "notify::active", lambda r, _: self._set_shadow_flag("on", r.get_active()) ) shadow_grp.add(shadow_on_row) soft_val = int(shadow_node.child_arg("softness") or 30) softness_adj = Gtk.Adjustment( value=soft_val, lower=0, upper=100, step_increment=1 ) softness_row = Adw.SpinRow( title="Softness (blur radius)", adjustment=softness_adj, digits=0 ) softness_row._last_val = soft_val def _on_soft_changed(r, _): new_val = int(r.get_value()) if new_val != getattr(r, "_last_val", None): r._last_val = new_val self._set_shadow("softness", new_val) softness_row.connect("notify::value", _on_soft_changed) shadow_grp.add(softness_row) spread_val = int(shadow_node.child_arg("spread") or 5) spread_adj = Gtk.Adjustment( value=spread_val, lower=-50, upper=100, step_increment=1 ) spread_row = Adw.SpinRow(title="Spread", adjustment=spread_adj, digits=0) spread_row._last_val = spread_val def _on_spread_changed(r, _): new_val = int(r.get_value()) if new_val != getattr(r, "_last_val", None): r._last_val = new_val self._set_shadow("spread", new_val) spread_row.connect("notify::value", _on_spread_changed) shadow_grp.add(spread_row) color_str = shadow_node.child_arg("color") or "#0007" color_row = Adw.ActionRow(title="Shadow Color") color_btn = Gtk.ColorDialogButton( dialog=Gtk.ColorDialog(title="Shadow Color", with_alpha=True) ) color_btn.set_rgba(_parse_color(color_str)) color_btn.set_valign(Gtk.Align.CENTER) color_btn.connect( "notify::rgba", lambda b, _: self._set_shadow_color(b.get_rgba()) ) color_row.add_suffix(color_btn) shadow_grp.add(color_row) draw_behind_row = Adw.SwitchRow( title="Draw Behind Window", subtitle="Fixes corner artifacts with non-CSD apps", ) draw_behind_row.set_active( shadow_node.get_child("draw-behind-window") is not None ) draw_behind_row.connect( "notify::active", lambda r, _: self._set_shadow_flag("draw-behind-window", r.get_active()), ) shadow_grp.add(draw_behind_row) content.append(shadow_grp) blur_grp = Adw.PreferencesGroup( title="Blur (Global)", description=( "Requires Niri 26.04 or later. Sets blur quality and optional " "window blur rules." ), ) blur_node = next((n for n in nodes if n.name == "blur"), None) blur_effects_row = Adw.SwitchRow( title="Enable Blur Effects", subtitle="Controls the compositor-level blur { off } setting", ) blur_effects_row.set_active(blur_effects_enabled(nodes)) blur_effects_row.connect( "notify::active", lambda r, _: self._set_blur_effects_enabled(r.get_active()), ) blur_grp.add(blur_effects_row) blur_enabled_row = Adw.SwitchRow( title="Force Blur on Windows", subtitle="Adds background-effect { blur true } to the global window rule", ) blur_enabled_row.set_active(global_window_blur_enabled(nodes)) blur_enabled_row.connect( "notify::active", lambda r, _: self._set_window_blur_enabled(r.get_active()), ) blur_grp.add(blur_enabled_row) focused_blur_row = Adw.SwitchRow( title="Keep Focused Windows Blurred", subtitle="Adds a focused-window rule that forces blur on", ) focused_blur_row.set_active(focused_window_blur_enabled(nodes)) focused_blur_row.connect( "notify::active", lambda r, _: self._set_focused_window_blur_enabled(r.get_active()), ) blur_grp.add(focused_blur_row) xray_row = Adw.SwitchRow( title="Use Xray Wallpaper Blur", subtitle="Use wallpaper-only blur; disable for regular background blur", ) xray_row.set_active(global_window_xray_enabled(nodes)) xray_row.connect( "notify::active", lambda r, _: self._set_window_blur_xray(r.get_active()), ) blur_grp.add(xray_row) opacity_val = get_global_window_opacity(nodes) opacity_adj = Gtk.Adjustment( value=opacity_val, lower=0.1, upper=1.0, step_increment=0.05 ) opacity_row = Adw.SpinRow( title="Window Opacity (1 = unset)", adjustment=opacity_adj, digits=2 ) opacity_row._last_val = opacity_val def _on_opacity_changed(r, _): new_val = round(float(r.get_value()), 2) if new_val != getattr(r, "_last_val", None): r._last_val = new_val self._set_window_opacity(new_val) opacity_row.connect("notify::value", _on_opacity_changed) blur_grp.add(opacity_row) border_bg_row = Adw.SwitchRow( title="Draw Border With Background", subtitle="Disable to avoid focus colors behind translucent windows", ) border_bg_row.set_active(get_global_draw_border_with_background(nodes)) border_bg_row.connect( "notify::active", lambda r, _: self._set_draw_border_with_background(r.get_active()), ) blur_grp.add(border_bg_row) passes_val = int(blur_node.child_arg("passes", 0) if blur_node else 0) passes_adj = Gtk.Adjustment( value=passes_val, lower=0, upper=10, step_increment=1 ) passes_row = Adw.SpinRow( title="Passes", adjustment=passes_adj, digits=0 ) passes_row._last_val = passes_val def _on_passes_changed(r, _): new_val = int(r.get_value()) if new_val != getattr(r, "_last_val", None): r._last_val = new_val self._set_blur("passes", new_val) passes_row.connect("notify::value", _on_passes_changed) blur_grp.add(passes_row) offset_val = float(blur_node.child_arg("offset", 2.0) if blur_node else 2.0) offset_adj = Gtk.Adjustment( value=offset_val, lower=0.0, upper=20.0, step_increment=0.1 ) offset_row = Adw.SpinRow(title="Offset", adjustment=offset_adj, digits=1) offset_row._last_val = offset_val def _on_offset_changed(r, _): new_val = float(r.get_value()) if new_val != getattr(r, "_last_val", None): r._last_val = new_val self._set_blur("offset", new_val) offset_row.connect("notify::value", _on_offset_changed) blur_grp.add(offset_row) noise_val = float(blur_node.child_arg("noise", 0.0) if blur_node else 0.0) noise_adj = Gtk.Adjustment( value=noise_val, lower=0.0, upper=1.0, step_increment=0.01 ) noise_row = Adw.SpinRow(title="Noise", adjustment=noise_adj, digits=2) noise_row._last_val = noise_val def _on_noise_changed(r, _): new_val = float(r.get_value()) if new_val != getattr(r, "_last_val", None): r._last_val = new_val self._set_blur("noise", new_val) noise_row.connect("notify::value", _on_noise_changed) blur_grp.add(noise_row) saturation_val = float(blur_node.child_arg("saturation", 1.0) if blur_node else 1.0) saturation_adj = Gtk.Adjustment( value=saturation_val, lower=0.0, upper=5.0, step_increment=0.1 ) saturation_row = Adw.SpinRow( title="Saturation", adjustment=saturation_adj, digits=1 ) saturation_row._last_val = saturation_val def _on_saturation_changed(r, _): new_val = float(r.get_value()) if new_val != getattr(r, "_last_val", None): r._last_val = new_val self._set_blur("saturation", new_val) saturation_row.connect("notify::value", _on_saturation_changed) blur_grp.add(saturation_row) content.append(blur_grp) misc_grp = Adw.PreferencesGroup(title="Window Geometry") cr_val = get_global_corner_radius(nodes) cr_adj = Gtk.Adjustment(value=cr_val, lower=0, upper=40, step_increment=1) cr_row = Adw.SpinRow( title="Corner Radius (px)", adjustment=cr_adj, digits=0, ) cr_row._last_val = cr_val def _on_cr_changed(r, _): new_val = int(r.get_value()) if new_val != getattr(r, "_last_val", None): r._last_val = new_val self._set_corner_radius(new_val) cr_row.connect("notify::value", _on_cr_changed) misc_grp.add(cr_row) content.append(misc_grp) def _build_border_group( self, title: str, key: str, node: KdlNode, layout: KdlNode ) -> Adw.PreferencesGroup: grp = Adw.PreferencesGroup(title=title) off_row = Adw.SwitchRow(title="Enable") off_row.set_active(node.get_child("off") is None) off_row.connect( "notify::active", lambda r, _, k=key: self._set_layout_border_flag( k, "off", not r.get_active() ), ) grp.add(off_row) width_val = int(node.child_arg("width") or 4) width_adj = Gtk.Adjustment(value=width_val, lower=1, upper=20, step_increment=1) width_row = Adw.SpinRow(title="Width (px)", adjustment=width_adj, digits=0) width_row._last_val = width_val def _on_width_changed(r, _, k=key): new_val = int(r.get_value()) if new_val != getattr(r, "_last_val", None): r._last_val = new_val self._set_layout_border(k, "width", new_val) width_row.connect("notify::value", _on_width_changed) grp.add(width_row) for color_key, color_label in [ ("active-color", "Active Color"), ("inactive-color", "Inactive Color"), ]: c_str = node.child_arg(color_key) or ( "#7fc8ff" if "active" in color_key else "#202020" ) c_row = Adw.ActionRow(title=color_label) c_btn = Gtk.ColorDialogButton( dialog=Gtk.ColorDialog(title=color_label, with_alpha=True) ) c_btn.set_rgba(_parse_color(c_str)) c_btn.set_valign(Gtk.Align.CENTER) c_btn.connect( "notify::rgba", lambda b, _, k=key, ck=color_key: self._set_layout_border( k, ck, self._rgba_to_hex(b.get_rgba()) ), ) c_row.add_suffix(c_btn) grp.add(c_row) return grp @staticmethod def _rgba_to_hex(rgba: Gdk.RGBA) -> str: r = int(rgba.red * 255) g = int(rgba.green * 255) b = int(rgba.blue * 255) a = int(rgba.alpha * 255) if a == 255: return f"#{r:02x}{g:02x}{b:02x}" return f"#{r:02x}{g:02x}{b:02x}{a:02x}" def _get_layout(self): return find_or_create(self._nodes, "layout") def _get_border_node(self, key: str) -> KdlNode: layout = self._get_layout() node = layout.get_child(key) if node is None: node = KdlNode(key) layout.children.append(node) return node def _set_layout_border(self, bkey: str, prop: str, value): node = self._get_border_node(bkey) set_child_arg(node, prop, value) self._commit(f"{bkey} {prop}") def _set_layout_border_flag(self, bkey: str, flag: str, enabled: bool): node = self._get_border_node(bkey) set_node_flag(node, flag, enabled) self._commit(f"{bkey} {flag}") def _get_shadow_node(self) -> KdlNode: layout = self._get_layout() node = layout.get_child("shadow") if node is None: node = KdlNode("shadow") layout.children.append(node) return node def _set_shadow(self, prop: str, value): set_child_arg(self._get_shadow_node(), prop, value) self._commit(f"shadow {prop}") def _set_shadow_flag(self, flag: str, enabled: bool): set_node_flag(self._get_shadow_node(), flag, enabled) self._commit(f"shadow {flag}") def _set_shadow_color(self, rgba: Gdk.RGBA): set_child_arg(self._get_shadow_node(), "color", self._rgba_to_hex(rgba)) self._commit("shadow color") def _set_blur(self, prop: str, value): blur_node = find_or_create(self._nodes, "blur") set_child_arg(blur_node, prop, value) self._commit(f"blur {prop}") def _set_blur_effects_enabled(self, enabled: bool): set_blur_effects_enabled(self._nodes, enabled) self._commit("blur effects") def _set_window_blur_enabled(self, enabled: bool): set_global_window_blur(self._nodes, enabled) self._commit("window blur") def _set_focused_window_blur_enabled(self, enabled: bool): set_focused_window_blur(self._nodes, enabled) self._commit("focused window blur") def _set_window_blur_xray(self, enabled: bool): set_global_window_xray(self._nodes, enabled) self._commit("window blur xray") def _set_window_opacity(self, opacity: float): set_global_window_opacity(self._nodes, opacity) self._commit("window opacity") def _set_draw_border_with_background(self, enabled: bool): set_global_draw_border_with_background(self._nodes, enabled) self._commit("draw border with background") def _set_corner_radius(self, radius: int): set_global_corner_radius(self._nodes, radius) self._commit("corner radius") def refresh(self): for child in list(self._content): self._content.remove(child) self._build_content()