@@ -118,7 +118,7 @@ def __init__(self, enabled: bool = True):
118118 self ._anim_id : Optional [int ] = None
119119 self ._phase = 0.0
120120 self ._gtk_ready = False
121- self ._layer_shell = None
121+ self ._use_layer_shell = False
122122
123123 try :
124124 self ._init_gtk_window ()
@@ -137,7 +137,6 @@ def _init_gtk_window(self) -> None:
137137 from gi .repository import Gdk , GLib , Gtk # noqa: F401
138138
139139 self ._GLib = GLib
140- self ._Gtk = Gtk
141140 self ._Gdk = Gdk
142141
143142 # TOPLEVEL (undecorated) positions more reliably than POPUP on Wayland
@@ -155,7 +154,6 @@ def _init_gtk_window(self) -> None:
155154 window .set_type_hint (Gdk .WindowTypeHint .NOTIFICATION )
156155 window .set_default_size (_OVERLAY_SIZE , _OVERLAY_SIZE )
157156 window .set_app_paintable (True )
158- window .set_events (0 ) # no input events needed
159157
160158 # RGBA visual for true transparency when the compositor supports it.
161159 screen = window .get_screen ()
@@ -164,25 +162,20 @@ def _init_gtk_window(self) -> None:
164162 window .set_visual (visual )
165163
166164 # Soft Wayland layer-shell (optional — never required).
167- self ._layer_shell = _try_import_layer_shell ()
168- self ._use_layer_shell = False
169- if self ._layer_shell is not None :
165+ layer_shell = _try_import_layer_shell ()
166+ if layer_shell is not None :
170167 try :
171- if self ._layer_shell .is_supported ():
172- self ._layer_shell .init_for_window (window )
173- self ._layer_shell .set_layer (window , self ._layer_shell .Layer .OVERLAY )
174- self ._layer_shell .set_anchor (window , self ._layer_shell .Edge .BOTTOM , True )
175- self ._layer_shell .set_margin (
176- window , self ._layer_shell .Edge .BOTTOM , _BOTTOM_MARGIN
177- )
178- self ._layer_shell .set_exclusive_zone (window , 0 )
168+ if layer_shell .is_supported ():
169+ layer_shell .init_for_window (window )
170+ layer_shell .set_layer (window , layer_shell .Layer .OVERLAY )
171+ layer_shell .set_anchor (window , layer_shell .Edge .BOTTOM , True )
172+ layer_shell .set_margin (window , layer_shell .Edge .BOTTOM , _BOTTOM_MARGIN )
173+ layer_shell .set_exclusive_zone (window , 0 )
179174 # Do not grab keyboard — passive visual only.
180- if hasattr (self ._layer_shell , "set_keyboard_mode" ):
181- self ._layer_shell .set_keyboard_mode (
182- window , self ._layer_shell .KeyboardMode .NONE
183- )
184- elif hasattr (self ._layer_shell , "set_keyboard_interactivity" ):
185- self ._layer_shell .set_keyboard_interactivity (window , False )
175+ if hasattr (layer_shell , "set_keyboard_mode" ):
176+ layer_shell .set_keyboard_mode (window , layer_shell .KeyboardMode .NONE )
177+ elif hasattr (layer_shell , "set_keyboard_interactivity" ):
178+ layer_shell .set_keyboard_interactivity (window , False )
186179 self ._use_layer_shell = True
187180 logger .info ("Dictation overlay using GtkLayerShell" )
188181 except Exception as e :
@@ -313,12 +306,9 @@ def _on_draw(self, widget, cr) -> bool:
313306 width = widget .get_allocated_width ()
314307 height = widget .get_allocated_height ()
315308 cx , cy = width / 2.0 , height / 2.0
316- base_r , base_g , base_b = _COLORS . get ( mode , _COLORS [ MODE_LISTENING ])
309+ base_r , base_g , base_b = _COLORS [ mode ]
317310
318- # Clear to fully transparent.
319- cr .set_operator (cr .get_operator ()) # keep default
320- cr .set_source_rgba (0 , 0 , 0 , 0 )
321- cr .set_operator (1 ) # CAIRO_OPERATOR_SOURCE / CLEAR-like full clear
311+ # Clear to fully transparent (SOURCE then OVER for compositors with alpha).
322312 try :
323313 import cairo as _cairo
324314
@@ -327,67 +317,40 @@ def _on_draw(self, widget, cr) -> bool:
327317 cr .paint ()
328318 cr .set_operator (_cairo .OPERATOR_OVER )
329319 except Exception :
320+ cr .set_source_rgba (0 , 0 , 0 , 0 )
330321 cr .paint ()
331322
332- # Pulse 0..1 → soft scale/alpha modulation.
333- pulse = 0.5 + 0.5 * math .sin (self ._phase * 2.0 * math .pi )
334- # Processing uses a slower, subtler pulse.
323+ # Pulse 0..1 → soft scale/alpha modulation (quieter while processing).
335324 if mode == MODE_PROCESSING :
336325 pulse = 0.55 + 0.25 * math .sin (self ._phase * 2.0 * math .pi )
326+ alpha_scale = 0.75
327+ core_a = 0.8
328+ else :
329+ pulse = 0.5 + 0.5 * math .sin (self ._phase * 2.0 * math .pi )
330+ alpha_scale = 1.0
331+ core_a = 0.95
337332
338333 # Outer glow rings.
339- for radius_scale , alpha_scale in ((0.95 , 0.12 ), (0.72 , 0.22 ), (0.52 , 0.35 )):
334+ for radius_scale , ring_a in ((0.95 , 0.12 ), (0.72 , 0.22 ), (0.52 , 0.35 )):
340335 radius = (width * 0.42 ) * radius_scale * (0.92 + 0.08 * pulse )
341- alpha = alpha_scale * (0.55 + 0.45 * pulse )
342- if mode == MODE_PROCESSING :
343- alpha *= 0.75
336+ alpha = ring_a * (0.55 + 0.45 * pulse ) * alpha_scale
344337 cr .set_source_rgba (base_r , base_g , base_b , alpha )
345338 cr .arc (cx , cy , radius , 0 , 2 * math .pi )
346339 cr .fill ()
347340
348- # Expanding ripple ring (listening only — stronger “hot mic” cue ).
341+ # Expanding ripple (listening only).
349342 if mode == MODE_LISTENING :
350- ripple_t = self ._phase
351- ripple_r = (width * 0.28 ) + ripple_t * (width * 0.28 )
352- ripple_a = max (0.0 , 0.45 * (1.0 - ripple_t ))
343+ ripple_r = (width * 0.28 ) + self ._phase * (width * 0.28 )
344+ ripple_a = max (0.0 , 0.45 * (1.0 - self ._phase ))
353345 cr .set_source_rgba (base_r , base_g , base_b , ripple_a )
354346 cr .set_line_width (2.0 )
355347 cr .arc (cx , cy , ripple_r , 0 , 2 * math .pi )
356348 cr .stroke ()
357349
358- # Solid core circle .
350+ # Solid core.
359351 core_r = width * 0.16 * (0.95 + 0.05 * pulse )
360- cr .set_source_rgba (base_r , base_g , base_b , 0.95 if mode == MODE_LISTENING else 0.8 )
352+ cr .set_source_rgba (base_r , base_g , base_b , core_a )
361353 cr .arc (cx , cy , core_r , 0 , 2 * math .pi )
362354 cr .fill ()
363355
364- # Simple mic glyph (stem + capsule) in white for readability.
365- cr .set_source_rgba (1 , 1 , 1 , 0.95 )
366- mic_w = width * 0.06
367- mic_h = height * 0.10
368- # Capsule body
369- cr .save ()
370- cr .translate (cx , cy - height * 0.02 )
371- cr .scale (1.0 , 1.15 )
372- cr .arc (0 , - mic_h * 0.15 , mic_w , math .pi , 0 )
373- cr .arc (0 , mic_h * 0.15 , mic_w , 0 , math .pi )
374- cr .close_path ()
375- cr .fill ()
376- cr .restore ()
377- # Stem
378- cr .set_line_width (max (1.5 , width * 0.02 ))
379- cr .set_line_cap (1 ) # round
380- try :
381- import cairo as _cairo
382-
383- cr .set_line_cap (_cairo .LINE_CAP_ROUND )
384- except Exception :
385- pass
386- cr .move_to (cx , cy + height * 0.06 )
387- cr .line_to (cx , cy + height * 0.14 )
388- cr .stroke ()
389- # Base arc under mic
390- cr .arc (cx , cy + height * 0.04 , width * 0.09 , 0.15 * math .pi , 0.85 * math .pi )
391- cr .stroke ()
392-
393356 return False
0 commit comments