@@ -1,900 +0,0 @@ |
| 1 | | -use std::sync::mpsc; |
| 2 | | - |
| 3 | | -use objc2::rc::Retained; |
| 4 | | -use objc2::{ |
| 5 | | - ClassType, DefinedClass, MainThreadMarker, MainThreadOnly, define_class, msg_send, sel, |
| 6 | | -}; |
| 7 | | -use objc2_app_kit::{ |
| 8 | | - NSBackingStoreType, NSButton, NSColorWell, NSPopUpButton, NSScrollView, NSSegmentedControl, |
| 9 | | - NSSlider, NSTextField, NSTextView, NSView, NSWindow, NSWindowStyleMask, |
| 10 | | -}; |
| 11 | | -use objc2_core_foundation::{CGPoint, CGRect, CGSize}; |
| 12 | | -use objc2_foundation::{NSObject, NSString}; |
| 13 | | - |
| 14 | | -/// Actions emitted by settings controls. |
| 15 | | -#[derive(Debug)] |
| 16 | | -pub enum SettingsAction { |
| 17 | | - GapInner(f64), |
| 18 | | - GapOuter(f64), |
| 19 | | - BarHeight(f64), |
| 20 | | - BorderWidth(f64), |
| 21 | | - BorderRadius(f64), |
| 22 | | - BorderColorFocused(String), |
| 23 | | - BorderColorUnfocused(String), |
| 24 | | - FocusFollowsMouse(bool), |
| 25 | | - MouseFollowsFocus(bool), |
| 26 | | - ModKey(String), |
| 27 | | - Open, |
| 28 | | -} |
| 29 | | - |
| 30 | | -/// Current settings snapshot for populating controls. |
| 31 | | -pub struct SettingsSnapshot { |
| 32 | | - pub gap_inner: f64, |
| 33 | | - pub gap_outer: f64, |
| 34 | | - pub bar_height: f64, |
| 35 | | - pub border_width: f64, |
| 36 | | - pub border_radius: f64, |
| 37 | | - pub border_color_focused: String, |
| 38 | | - pub border_color_unfocused: String, |
| 39 | | - pub focus_follows_mouse: bool, |
| 40 | | - pub mouse_follows_focus: bool, |
| 41 | | - pub mod_key: String, |
| 42 | | - pub keybinds: Vec<(String, String)>, // (key combo, action) |
| 43 | | - pub rules: Vec<(String, String)>, // (match, action) |
| 44 | | -} |
| 45 | | - |
| 46 | | -// --- ObjC action handler --- |
| 47 | | - |
| 48 | | -struct SettingsHandlerIvars { |
| 49 | | - tx: mpsc::Sender<SettingsAction>, |
| 50 | | - tab_views: std::cell::RefCell<Vec<Retained<NSView>>>, |
| 51 | | - content_container: std::cell::RefCell<Option<Retained<NSView>>>, |
| 52 | | -} |
| 53 | | - |
| 54 | | -impl SettingsHandler { |
| 55 | | - fn new(mtm: MainThreadMarker, tx: mpsc::Sender<SettingsAction>) -> Retained<Self> { |
| 56 | | - let this = mtm.alloc().set_ivars(SettingsHandlerIvars { |
| 57 | | - tx, |
| 58 | | - tab_views: std::cell::RefCell::new(Vec::new()), |
| 59 | | - content_container: std::cell::RefCell::new(None), |
| 60 | | - }); |
| 61 | | - unsafe { msg_send![super(this), init] } |
| 62 | | - } |
| 63 | | - |
| 64 | | - fn emit(&self, action: SettingsAction) { |
| 65 | | - let _ = self.ivars().tx.send(action); |
| 66 | | - } |
| 67 | | - |
| 68 | | - fn set_tab_views(&self, views: Vec<Retained<NSView>>, container: Retained<NSView>) { |
| 69 | | - *self.ivars().tab_views.borrow_mut() = views; |
| 70 | | - *self.ivars().content_container.borrow_mut() = Some(container); |
| 71 | | - } |
| 72 | | - |
| 73 | | - fn switch_to_tab(&self, index: usize) { |
| 74 | | - let views = self.ivars().tab_views.borrow(); |
| 75 | | - let container_ref = self.ivars().content_container.borrow(); |
| 76 | | - let Some(container) = container_ref.as_ref() else { |
| 77 | | - return; |
| 78 | | - }; |
| 79 | | - // Remove all subviews from container |
| 80 | | - let subviews = container.subviews(); |
| 81 | | - for sv in subviews.iter() { |
| 82 | | - sv.removeFromSuperview(); |
| 83 | | - } |
| 84 | | - // Add the selected tab view |
| 85 | | - if let Some(view) = views.get(index) { |
| 86 | | - container.addSubview(view); |
| 87 | | - } |
| 88 | | - } |
| 89 | | -} |
| 90 | | - |
| 91 | | -define_class!( |
| 92 | | - #[unsafe(super(NSObject))] |
| 93 | | - #[thread_kind = MainThreadOnly] |
| 94 | | - #[name = "TarmacSettingsHandler"] |
| 95 | | - #[ivars = SettingsHandlerIvars] |
| 96 | | - struct SettingsHandler; |
| 97 | | - |
| 98 | | - impl SettingsHandler { |
| 99 | | - #[unsafe(method(onTabChanged:))] |
| 100 | | - fn on_tab_changed(&self, sender: Option<&NSSegmentedControl>) { |
| 101 | | - if let Some(seg) = sender { |
| 102 | | - let idx = seg.selectedSegment() as usize; |
| 103 | | - self.switch_to_tab(idx); |
| 104 | | - } |
| 105 | | - } |
| 106 | | - |
| 107 | | - #[unsafe(method(onGapInnerChanged:))] |
| 108 | | - fn on_gap_inner(&self, sender: Option<&NSSlider>) { |
| 109 | | - if let Some(s) = sender { |
| 110 | | - self.emit(SettingsAction::GapInner(s.doubleValue())); |
| 111 | | - } |
| 112 | | - } |
| 113 | | - |
| 114 | | - #[unsafe(method(onGapOuterChanged:))] |
| 115 | | - fn on_gap_outer(&self, sender: Option<&NSSlider>) { |
| 116 | | - if let Some(s) = sender { |
| 117 | | - self.emit(SettingsAction::GapOuter(s.doubleValue())); |
| 118 | | - } |
| 119 | | - } |
| 120 | | - |
| 121 | | - #[unsafe(method(onBarHeightChanged:))] |
| 122 | | - fn on_bar_height(&self, sender: Option<&NSSlider>) { |
| 123 | | - if let Some(s) = sender { |
| 124 | | - self.emit(SettingsAction::BarHeight(s.doubleValue())); |
| 125 | | - } |
| 126 | | - } |
| 127 | | - |
| 128 | | - #[unsafe(method(onBorderWidthChanged:))] |
| 129 | | - fn on_border_width(&self, sender: Option<&NSSlider>) { |
| 130 | | - if let Some(s) = sender { |
| 131 | | - self.emit(SettingsAction::BorderWidth(s.doubleValue())); |
| 132 | | - } |
| 133 | | - } |
| 134 | | - |
| 135 | | - #[unsafe(method(onBorderRadiusChanged:))] |
| 136 | | - fn on_border_radius(&self, sender: Option<&NSSlider>) { |
| 137 | | - if let Some(s) = sender { |
| 138 | | - self.emit(SettingsAction::BorderRadius(s.doubleValue())); |
| 139 | | - } |
| 140 | | - } |
| 141 | | - |
| 142 | | - #[unsafe(method(onBorderColorFocusedChanged:))] |
| 143 | | - fn on_border_color_focused(&self, sender: Option<&NSColorWell>) { |
| 144 | | - if let Some(well) = sender { |
| 145 | | - let hex = color_well_to_hex(well); |
| 146 | | - self.emit(SettingsAction::BorderColorFocused(hex)); |
| 147 | | - } |
| 148 | | - } |
| 149 | | - |
| 150 | | - #[unsafe(method(onBorderColorUnfocusedChanged:))] |
| 151 | | - fn on_border_color_unfocused(&self, sender: Option<&NSColorWell>) { |
| 152 | | - if let Some(well) = sender { |
| 153 | | - let hex = color_well_to_hex(well); |
| 154 | | - self.emit(SettingsAction::BorderColorUnfocused(hex)); |
| 155 | | - } |
| 156 | | - } |
| 157 | | - |
| 158 | | - #[unsafe(method(onFfmToggled:))] |
| 159 | | - fn on_ffm_toggled(&self, sender: Option<&NSButton>) { |
| 160 | | - if let Some(btn) = sender { |
| 161 | | - self.emit(SettingsAction::FocusFollowsMouse(btn.state() == 1)); |
| 162 | | - } |
| 163 | | - } |
| 164 | | - |
| 165 | | - #[unsafe(method(onMffToggled:))] |
| 166 | | - fn on_mff_toggled(&self, sender: Option<&NSButton>) { |
| 167 | | - if let Some(btn) = sender { |
| 168 | | - self.emit(SettingsAction::MouseFollowsFocus(btn.state() == 1)); |
| 169 | | - } |
| 170 | | - } |
| 171 | | - |
| 172 | | - #[unsafe(method(onModKeyChanged:))] |
| 173 | | - fn on_mod_key(&self, sender: Option<&NSPopUpButton>) { |
| 174 | | - if let Some(popup) = sender { |
| 175 | | - let idx = popup.indexOfSelectedItem(); |
| 176 | | - let key = match idx { |
| 177 | | - 0 => "command", |
| 178 | | - 1 => "option", |
| 179 | | - 2 => "control", |
| 180 | | - _ => "command", |
| 181 | | - }; |
| 182 | | - self.emit(SettingsAction::ModKey(key.to_string())); |
| 183 | | - } |
| 184 | | - } |
| 185 | | - } |
| 186 | | -); |
| 187 | | - |
| 188 | | -// --- Constants --- |
| 189 | | - |
| 190 | | -const WIN_W: f64 = 480.0; |
| 191 | | -const WIN_H: f64 = 520.0; |
| 192 | | -const TAB_BAR_H: f64 = 36.0; |
| 193 | | -const CONTENT_H: f64 = WIN_H - TAB_BAR_H; |
| 194 | | - |
| 195 | | -// --- SettingsWindow --- |
| 196 | | - |
| 197 | | -pub struct SettingsWindow { |
| 198 | | - window: Retained<NSWindow>, |
| 199 | | - _handler: Retained<SettingsHandler>, |
| 200 | | - action_rx: mpsc::Receiver<SettingsAction>, |
| 201 | | - // General tab controls |
| 202 | | - gap_inner_slider: Retained<NSSlider>, |
| 203 | | - gap_outer_slider: Retained<NSSlider>, |
| 204 | | - bar_height_slider: Retained<NSSlider>, |
| 205 | | - border_width_slider: Retained<NSSlider>, |
| 206 | | - border_radius_slider: Retained<NSSlider>, |
| 207 | | - focused_color_well: Retained<NSColorWell>, |
| 208 | | - unfocused_color_well: Retained<NSColorWell>, |
| 209 | | - ffm_checkbox: Retained<NSButton>, |
| 210 | | - mff_checkbox: Retained<NSButton>, |
| 211 | | - mod_key_popup: Retained<NSPopUpButton>, |
| 212 | | - gap_inner_label: Retained<NSTextField>, |
| 213 | | - gap_outer_label: Retained<NSTextField>, |
| 214 | | - bar_height_label: Retained<NSTextField>, |
| 215 | | - border_width_label: Retained<NSTextField>, |
| 216 | | - border_radius_label: Retained<NSTextField>, |
| 217 | | - // Keybindings/Rules text views for repopulating |
| 218 | | - keybinds_text: Retained<NSTextView>, |
| 219 | | - rules_text: Retained<NSTextView>, |
| 220 | | -} |
| 221 | | - |
| 222 | | -impl SettingsWindow { |
| 223 | | - pub fn new(mtm: MainThreadMarker) -> Self { |
| 224 | | - let (tx, rx) = mpsc::channel(); |
| 225 | | - let handler = SettingsHandler::new(mtm, tx); |
| 226 | | - |
| 227 | | - let style = NSWindowStyleMask::Titled |
| 228 | | - | NSWindowStyleMask::Closable |
| 229 | | - | NSWindowStyleMask::Miniaturizable; |
| 230 | | - |
| 231 | | - let frame = CGRect::new(CGPoint::new(200.0, 200.0), CGSize::new(WIN_W, WIN_H)); |
| 232 | | - let window: Retained<NSWindow> = unsafe { |
| 233 | | - msg_send![ |
| 234 | | - NSWindow::alloc(mtm), |
| 235 | | - initWithContentRect: frame, |
| 236 | | - styleMask: style, |
| 237 | | - backing: NSBackingStoreType::Buffered, |
| 238 | | - defer: false |
| 239 | | - ] |
| 240 | | - }; |
| 241 | | - let title = NSString::from_str("tarmac Settings"); |
| 242 | | - window.setTitle(&title); |
| 243 | | - window.center(); |
| 244 | | - unsafe { window.setReleasedWhenClosed(false) }; |
| 245 | | - |
| 246 | | - // Root content view |
| 247 | | - let root: Retained<NSView> = unsafe { msg_send![NSView::alloc(mtm), initWithFrame: frame] }; |
| 248 | | - window.setContentView(Some(&root)); |
| 249 | | - |
| 250 | | - // Segmented control (tab bar) at top |
| 251 | | - let seg_frame = CGRect::new( |
| 252 | | - CGPoint::new(20.0, WIN_H - TAB_BAR_H + 4.0), |
| 253 | | - CGSize::new(WIN_W - 40.0, 24.0), |
| 254 | | - ); |
| 255 | | - let seg: Retained<NSSegmentedControl> = |
| 256 | | - unsafe { msg_send![NSSegmentedControl::alloc(mtm), initWithFrame: seg_frame] }; |
| 257 | | - seg.setSegmentCount(4); |
| 258 | | - set_segment_label(&seg, 0, "General"); |
| 259 | | - set_segment_label(&seg, 1, "Keybindings"); |
| 260 | | - set_segment_label(&seg, 2, "Rules"); |
| 261 | | - set_segment_label(&seg, 3, "About"); |
| 262 | | - for i in 0..4 { |
| 263 | | - seg.setWidth_forSegment(100.0, i); |
| 264 | | - } |
| 265 | | - seg.setSelectedSegment(0); |
| 266 | | - unsafe { |
| 267 | | - seg.setTarget(Some(&*handler)); |
| 268 | | - seg.setAction(Some(sel!(onTabChanged:))); |
| 269 | | - } |
| 270 | | - root.addSubview(&seg); |
| 271 | | - |
| 272 | | - // Content container (below tab bar) |
| 273 | | - let container_frame = CGRect::new(CGPoint::new(0.0, 0.0), CGSize::new(WIN_W, CONTENT_H)); |
| 274 | | - let container: Retained<NSView> = |
| 275 | | - unsafe { msg_send![NSView::alloc(mtm), initWithFrame: container_frame] }; |
| 276 | | - root.addSubview(&container); |
| 277 | | - |
| 278 | | - // Build tab views |
| 279 | | - let general = build_general_view(mtm, &handler); |
| 280 | | - let (keybinds_view, keybinds_text) = build_text_tab(mtm, "No keybindings loaded."); |
| 281 | | - let (rules_view, rules_text) = build_text_tab(mtm, "No rules loaded."); |
| 282 | | - let about_view = build_about_view(mtm); |
| 283 | | - |
| 284 | | - // Register tab views with handler for switching |
| 285 | | - handler.set_tab_views( |
| 286 | | - vec![general.view.clone(), keybinds_view, rules_view, about_view], |
| 287 | | - container.clone(), |
| 288 | | - ); |
| 289 | | - |
| 290 | | - // Show General tab initially |
| 291 | | - container.addSubview(&general.view); |
| 292 | | - |
| 293 | | - Self { |
| 294 | | - window, |
| 295 | | - _handler: handler, |
| 296 | | - action_rx: rx, |
| 297 | | - gap_inner_slider: general.gap_inner_slider, |
| 298 | | - gap_outer_slider: general.gap_outer_slider, |
| 299 | | - bar_height_slider: general.bar_height_slider, |
| 300 | | - border_width_slider: general.border_width_slider, |
| 301 | | - border_radius_slider: general.border_radius_slider, |
| 302 | | - focused_color_well: general.focused_color_well, |
| 303 | | - unfocused_color_well: general.unfocused_color_well, |
| 304 | | - ffm_checkbox: general.ffm_checkbox, |
| 305 | | - mff_checkbox: general.mff_checkbox, |
| 306 | | - mod_key_popup: general.mod_key_popup, |
| 307 | | - gap_inner_label: general.gap_inner_label, |
| 308 | | - gap_outer_label: general.gap_outer_label, |
| 309 | | - bar_height_label: general.bar_height_label, |
| 310 | | - border_width_label: general.border_width_label, |
| 311 | | - border_radius_label: general.border_radius_label, |
| 312 | | - keybinds_text, |
| 313 | | - rules_text, |
| 314 | | - } |
| 315 | | - } |
| 316 | | - |
| 317 | | - pub fn toggle(&self) { |
| 318 | | - if self.window.isVisible() { |
| 319 | | - self.window.orderOut(None); |
| 320 | | - } else { |
| 321 | | - self.window.makeKeyAndOrderFront(None); |
| 322 | | - } |
| 323 | | - } |
| 324 | | - |
| 325 | | - pub fn show(&self) { |
| 326 | | - self.window.makeKeyAndOrderFront(None); |
| 327 | | - } |
| 328 | | - |
| 329 | | - pub fn populate(&self, snap: &SettingsSnapshot) { |
| 330 | | - // General tab |
| 331 | | - self.gap_inner_slider.setDoubleValue(snap.gap_inner); |
| 332 | | - self.gap_outer_slider.setDoubleValue(snap.gap_outer); |
| 333 | | - self.bar_height_slider.setDoubleValue(snap.bar_height); |
| 334 | | - self.border_width_slider.setDoubleValue(snap.border_width); |
| 335 | | - self.border_radius_slider.setDoubleValue(snap.border_radius); |
| 336 | | - |
| 337 | | - set_value_label(&self.gap_inner_label, snap.gap_inner); |
| 338 | | - set_value_label(&self.gap_outer_label, snap.gap_outer); |
| 339 | | - set_value_label(&self.bar_height_label, snap.bar_height); |
| 340 | | - set_value_label(&self.border_width_label, snap.border_width); |
| 341 | | - set_value_label(&self.border_radius_label, snap.border_radius); |
| 342 | | - |
| 343 | | - set_color_well(&self.focused_color_well, &snap.border_color_focused); |
| 344 | | - set_color_well(&self.unfocused_color_well, &snap.border_color_unfocused); |
| 345 | | - |
| 346 | | - self.ffm_checkbox |
| 347 | | - .setState(if snap.focus_follows_mouse { 1 } else { 0 }); |
| 348 | | - self.mff_checkbox |
| 349 | | - .setState(if snap.mouse_follows_focus { 1 } else { 0 }); |
| 350 | | - |
| 351 | | - let mod_idx: isize = match snap.mod_key.as_str() { |
| 352 | | - "command" | "cmd" => 0, |
| 353 | | - "option" | "alt" => 1, |
| 354 | | - "control" | "ctrl" => 2, |
| 355 | | - _ => 0, |
| 356 | | - }; |
| 357 | | - self.mod_key_popup.selectItemAtIndex(mod_idx); |
| 358 | | - |
| 359 | | - // Keybindings tab |
| 360 | | - let mut kb_text = String::new(); |
| 361 | | - for (keys, action) in &snap.keybinds { |
| 362 | | - kb_text.push_str(&format!("{:<28} {}\n", keys, action)); |
| 363 | | - } |
| 364 | | - if kb_text.is_empty() { |
| 365 | | - kb_text.push_str("No keybindings configured."); |
| 366 | | - } |
| 367 | | - set_text_view(&self.keybinds_text, &kb_text); |
| 368 | | - |
| 369 | | - // Rules tab |
| 370 | | - let mut rules_text = String::new(); |
| 371 | | - for (match_str, action_str) in &snap.rules { |
| 372 | | - rules_text.push_str(&format!("{:<28} {}\n", match_str, action_str)); |
| 373 | | - } |
| 374 | | - if rules_text.is_empty() { |
| 375 | | - rules_text.push_str("No window rules configured."); |
| 376 | | - } |
| 377 | | - set_text_view(&self.rules_text, &rules_text); |
| 378 | | - } |
| 379 | | - |
| 380 | | - pub fn poll_actions(&self) -> Vec<SettingsAction> { |
| 381 | | - let mut actions = Vec::new(); |
| 382 | | - while let Ok(action) = self.action_rx.try_recv() { |
| 383 | | - actions.push(action); |
| 384 | | - } |
| 385 | | - actions |
| 386 | | - } |
| 387 | | - |
| 388 | | - pub fn refresh_labels(&self) { |
| 389 | | - set_value_label(&self.gap_inner_label, self.gap_inner_slider.doubleValue()); |
| 390 | | - set_value_label(&self.gap_outer_label, self.gap_outer_slider.doubleValue()); |
| 391 | | - set_value_label(&self.bar_height_label, self.bar_height_slider.doubleValue()); |
| 392 | | - set_value_label( |
| 393 | | - &self.border_width_label, |
| 394 | | - self.border_width_slider.doubleValue(), |
| 395 | | - ); |
| 396 | | - set_value_label( |
| 397 | | - &self.border_radius_label, |
| 398 | | - self.border_radius_slider.doubleValue(), |
| 399 | | - ); |
| 400 | | - } |
| 401 | | -} |
| 402 | | - |
| 403 | | -// --- General tab builder --- |
| 404 | | - |
| 405 | | -struct GeneralTab { |
| 406 | | - view: Retained<NSView>, |
| 407 | | - gap_inner_slider: Retained<NSSlider>, |
| 408 | | - gap_outer_slider: Retained<NSSlider>, |
| 409 | | - bar_height_slider: Retained<NSSlider>, |
| 410 | | - border_width_slider: Retained<NSSlider>, |
| 411 | | - border_radius_slider: Retained<NSSlider>, |
| 412 | | - focused_color_well: Retained<NSColorWell>, |
| 413 | | - unfocused_color_well: Retained<NSColorWell>, |
| 414 | | - ffm_checkbox: Retained<NSButton>, |
| 415 | | - mff_checkbox: Retained<NSButton>, |
| 416 | | - mod_key_popup: Retained<NSPopUpButton>, |
| 417 | | - gap_inner_label: Retained<NSTextField>, |
| 418 | | - gap_outer_label: Retained<NSTextField>, |
| 419 | | - bar_height_label: Retained<NSTextField>, |
| 420 | | - border_width_label: Retained<NSTextField>, |
| 421 | | - border_radius_label: Retained<NSTextField>, |
| 422 | | -} |
| 423 | | - |
| 424 | | -fn build_general_view(mtm: MainThreadMarker, handler: &SettingsHandler) -> GeneralTab { |
| 425 | | - let frame = CGRect::new(CGPoint::new(0.0, 0.0), CGSize::new(WIN_W, CONTENT_H)); |
| 426 | | - let view: Retained<NSView> = unsafe { msg_send![NSView::alloc(mtm), initWithFrame: frame] }; |
| 427 | | - |
| 428 | | - let lx = 20.0_f64; |
| 429 | | - let cx = 180.0_f64; |
| 430 | | - let cw = 200.0_f64; |
| 431 | | - let row = 32.0_f64; |
| 432 | | - let mut y = CONTENT_H - 30.0; |
| 433 | | - |
| 434 | | - // Layout |
| 435 | | - add_section_label(mtm, &view, "Layout", lx, y); |
| 436 | | - y -= row; |
| 437 | | - let gap_inner_label = add_value_label(mtm, &view, "0", cx + cw + 8.0, y); |
| 438 | | - let gap_inner_slider = add_slider( |
| 439 | | - mtm, |
| 440 | | - &view, |
| 441 | | - handler, |
| 442 | | - "Inner Gap", |
| 443 | | - lx, |
| 444 | | - cx, |
| 445 | | - y, |
| 446 | | - cw, |
| 447 | | - 0.0, |
| 448 | | - 50.0, |
| 449 | | - 0.0, |
| 450 | | - sel!(onGapInnerChanged:), |
| 451 | | - ); |
| 452 | | - y -= row; |
| 453 | | - let gap_outer_label = add_value_label(mtm, &view, "0", cx + cw + 8.0, y); |
| 454 | | - let gap_outer_slider = add_slider( |
| 455 | | - mtm, |
| 456 | | - &view, |
| 457 | | - handler, |
| 458 | | - "Outer Gap", |
| 459 | | - lx, |
| 460 | | - cx, |
| 461 | | - y, |
| 462 | | - cw, |
| 463 | | - 0.0, |
| 464 | | - 50.0, |
| 465 | | - 0.0, |
| 466 | | - sel!(onGapOuterChanged:), |
| 467 | | - ); |
| 468 | | - y -= row; |
| 469 | | - let bar_height_label = add_value_label(mtm, &view, "0", cx + cw + 8.0, y); |
| 470 | | - let bar_height_slider = add_slider( |
| 471 | | - mtm, |
| 472 | | - &view, |
| 473 | | - handler, |
| 474 | | - "Bar Height", |
| 475 | | - lx, |
| 476 | | - cx, |
| 477 | | - y, |
| 478 | | - cw, |
| 479 | | - 0.0, |
| 480 | | - 60.0, |
| 481 | | - 0.0, |
| 482 | | - sel!(onBarHeightChanged:), |
| 483 | | - ); |
| 484 | | - y -= row + 12.0; |
| 485 | | - |
| 486 | | - // Borders |
| 487 | | - add_section_label(mtm, &view, "Borders", lx, y); |
| 488 | | - y -= row; |
| 489 | | - let border_width_label = add_value_label(mtm, &view, "0", cx + cw + 8.0, y); |
| 490 | | - let border_width_slider = add_slider( |
| 491 | | - mtm, |
| 492 | | - &view, |
| 493 | | - handler, |
| 494 | | - "Width", |
| 495 | | - lx, |
| 496 | | - cx, |
| 497 | | - y, |
| 498 | | - cw, |
| 499 | | - 0.0, |
| 500 | | - 10.0, |
| 501 | | - 0.0, |
| 502 | | - sel!(onBorderWidthChanged:), |
| 503 | | - ); |
| 504 | | - y -= row; |
| 505 | | - let border_radius_label = add_value_label(mtm, &view, "0", cx + cw + 8.0, y); |
| 506 | | - let border_radius_slider = add_slider( |
| 507 | | - mtm, |
| 508 | | - &view, |
| 509 | | - handler, |
| 510 | | - "Radius", |
| 511 | | - lx, |
| 512 | | - cx, |
| 513 | | - y, |
| 514 | | - cw, |
| 515 | | - 0.0, |
| 516 | | - 30.0, |
| 517 | | - 10.0, |
| 518 | | - sel!(onBorderRadiusChanged:), |
| 519 | | - ); |
| 520 | | - y -= row; |
| 521 | | - add_label(mtm, &view, "Focused Color", lx, y); |
| 522 | | - let focused_color_well = add_color_well( |
| 523 | | - mtm, |
| 524 | | - &view, |
| 525 | | - handler, |
| 526 | | - cx, |
| 527 | | - y, |
| 528 | | - sel!(onBorderColorFocusedChanged:), |
| 529 | | - ); |
| 530 | | - y -= row; |
| 531 | | - add_label(mtm, &view, "Unfocused Color", lx, y); |
| 532 | | - let unfocused_color_well = add_color_well( |
| 533 | | - mtm, |
| 534 | | - &view, |
| 535 | | - handler, |
| 536 | | - cx, |
| 537 | | - y, |
| 538 | | - sel!(onBorderColorUnfocusedChanged:), |
| 539 | | - ); |
| 540 | | - y -= row + 12.0; |
| 541 | | - |
| 542 | | - // Behavior |
| 543 | | - add_section_label(mtm, &view, "Behavior", lx, y); |
| 544 | | - y -= row; |
| 545 | | - let ffm_checkbox = add_checkbox( |
| 546 | | - mtm, |
| 547 | | - &view, |
| 548 | | - handler, |
| 549 | | - "Focus follows mouse", |
| 550 | | - lx, |
| 551 | | - y, |
| 552 | | - sel!(onFfmToggled:), |
| 553 | | - ); |
| 554 | | - y -= row; |
| 555 | | - let mff_checkbox = add_checkbox( |
| 556 | | - mtm, |
| 557 | | - &view, |
| 558 | | - handler, |
| 559 | | - "Mouse follows focus", |
| 560 | | - lx, |
| 561 | | - y, |
| 562 | | - sel!(onMffToggled:), |
| 563 | | - ); |
| 564 | | - y -= row + 12.0; |
| 565 | | - |
| 566 | | - // Modifier Key |
| 567 | | - add_section_label(mtm, &view, "Modifier Key", lx, y); |
| 568 | | - y -= row; |
| 569 | | - let mod_key_popup = add_popup( |
| 570 | | - mtm, |
| 571 | | - &view, |
| 572 | | - handler, |
| 573 | | - lx, |
| 574 | | - y, |
| 575 | | - cw, |
| 576 | | - &["Command", "Option", "Control"], |
| 577 | | - sel!(onModKeyChanged:), |
| 578 | | - ); |
| 579 | | - let _ = y; |
| 580 | | - |
| 581 | | - GeneralTab { |
| 582 | | - view, |
| 583 | | - gap_inner_slider, |
| 584 | | - gap_outer_slider, |
| 585 | | - bar_height_slider, |
| 586 | | - border_width_slider, |
| 587 | | - border_radius_slider, |
| 588 | | - focused_color_well, |
| 589 | | - unfocused_color_well, |
| 590 | | - ffm_checkbox, |
| 591 | | - mff_checkbox, |
| 592 | | - mod_key_popup, |
| 593 | | - gap_inner_label, |
| 594 | | - gap_outer_label, |
| 595 | | - bar_height_label, |
| 596 | | - border_width_label, |
| 597 | | - border_radius_label, |
| 598 | | - } |
| 599 | | -} |
| 600 | | - |
| 601 | | -// --- Text-based tabs (Keybindings, Rules) --- |
| 602 | | - |
| 603 | | -fn build_text_tab( |
| 604 | | - mtm: MainThreadMarker, |
| 605 | | - placeholder: &str, |
| 606 | | -) -> (Retained<NSView>, Retained<NSTextView>) { |
| 607 | | - let frame = CGRect::new(CGPoint::new(0.0, 0.0), CGSize::new(WIN_W, CONTENT_H)); |
| 608 | | - let view: Retained<NSView> = unsafe { msg_send![NSView::alloc(mtm), initWithFrame: frame] }; |
| 609 | | - |
| 610 | | - let scroll_frame = CGRect::new( |
| 611 | | - CGPoint::new(15.0, 15.0), |
| 612 | | - CGSize::new(WIN_W - 30.0, CONTENT_H - 30.0), |
| 613 | | - ); |
| 614 | | - let scroll: Retained<NSScrollView> = |
| 615 | | - unsafe { msg_send![NSScrollView::alloc(mtm), initWithFrame: scroll_frame] }; |
| 616 | | - scroll.setHasVerticalScroller(true); |
| 617 | | - scroll.setBorderType(objc2_app_kit::NSBorderType(2)); // NSBezelBorder |
| 618 | | - |
| 619 | | - let text_frame = CGRect::new(CGPoint::new(0.0, 0.0), CGSize::new(WIN_W - 50.0, CONTENT_H)); |
| 620 | | - let text: Retained<NSTextView> = |
| 621 | | - unsafe { msg_send![NSTextView::alloc(mtm), initWithFrame: text_frame] }; |
| 622 | | - text.setEditable(false); |
| 623 | | - unsafe { |
| 624 | | - let mono: Retained<objc2_app_kit::NSFont> = msg_send![objc2_app_kit::NSFont::class(), monospacedSystemFontOfSize: 11.0_f64, weight: 0.0_f64]; |
| 625 | | - text.setFont(Some(&mono)); |
| 626 | | - } |
| 627 | | - set_text_view(&text, placeholder); |
| 628 | | - |
| 629 | | - scroll.setDocumentView(Some(&text)); |
| 630 | | - view.addSubview(&scroll); |
| 631 | | - |
| 632 | | - (view, text) |
| 633 | | -} |
| 634 | | - |
| 635 | | -fn set_text_view(tv: &NSTextView, content: &str) { |
| 636 | | - let ns = NSString::from_str(content); |
| 637 | | - tv.setString(&ns); |
| 638 | | -} |
| 639 | | - |
| 640 | | -// --- About tab --- |
| 641 | | - |
| 642 | | -fn build_about_view(mtm: MainThreadMarker) -> Retained<NSView> { |
| 643 | | - let frame = CGRect::new(CGPoint::new(0.0, 0.0), CGSize::new(WIN_W, CONTENT_H)); |
| 644 | | - let view: Retained<NSView> = unsafe { msg_send![NSView::alloc(mtm), initWithFrame: frame] }; |
| 645 | | - |
| 646 | | - let center_x = WIN_W / 2.0 - 150.0; |
| 647 | | - let mut y = CONTENT_H - 60.0; |
| 648 | | - |
| 649 | | - // App name |
| 650 | | - let name_frame = CGRect::new(CGPoint::new(center_x, y), CGSize::new(300.0, 28.0)); |
| 651 | | - let name: Retained<NSTextField> = |
| 652 | | - unsafe { msg_send![NSTextField::alloc(mtm), initWithFrame: name_frame] }; |
| 653 | | - name.setStringValue(&NSString::from_str("tarmac")); |
| 654 | | - name.setEditable(false); |
| 655 | | - name.setBordered(false); |
| 656 | | - name.setDrawsBackground(false); |
| 657 | | - unsafe { |
| 658 | | - let _: () = msg_send![&*name, setAlignment: 1_isize]; // NSTextAlignmentCenter |
| 659 | | - let font: Retained<objc2_app_kit::NSFont> = |
| 660 | | - msg_send![objc2_app_kit::NSFont::class(), boldSystemFontOfSize: 24.0_f64]; |
| 661 | | - name.setFont(Some(&font)); |
| 662 | | - } |
| 663 | | - view.addSubview(&name); |
| 664 | | - y -= 28.0; |
| 665 | | - |
| 666 | | - // Subtitle |
| 667 | | - add_centered_label(mtm, &view, "a macOS tiling window manager", center_x, y); |
| 668 | | - y -= 36.0; |
| 669 | | - |
| 670 | | - // Version |
| 671 | | - let version = format!("Version {}", env!("CARGO_PKG_VERSION")); |
| 672 | | - add_centered_label(mtm, &view, &version, center_x, y); |
| 673 | | - y -= 28.0; |
| 674 | | - |
| 675 | | - // Build info |
| 676 | | - add_centered_label( |
| 677 | | - mtm, |
| 678 | | - &view, |
| 679 | | - "Rust 2024 edition · objc2 + SkyLight", |
| 680 | | - center_x, |
| 681 | | - y, |
| 682 | | - ); |
| 683 | | - y -= 36.0; |
| 684 | | - |
| 685 | | - // Links |
| 686 | | - add_centered_label(mtm, &view, "github.com/gardesk/tarmac", center_x, y); |
| 687 | | - y -= 28.0; |
| 688 | | - add_centered_label(mtm, &view, "config: ~/.config/tarmac/init.lua", center_x, y); |
| 689 | | - let _ = y; |
| 690 | | - |
| 691 | | - view |
| 692 | | -} |
| 693 | | - |
| 694 | | -fn add_centered_label(mtm: MainThreadMarker, parent: &NSView, text: &str, x: f64, y: f64) { |
| 695 | | - let frame = CGRect::new(CGPoint::new(x, y), CGSize::new(300.0, 20.0)); |
| 696 | | - let label: Retained<NSTextField> = |
| 697 | | - unsafe { msg_send![NSTextField::alloc(mtm), initWithFrame: frame] }; |
| 698 | | - label.setStringValue(&NSString::from_str(text)); |
| 699 | | - label.setEditable(false); |
| 700 | | - label.setBordered(false); |
| 701 | | - label.setDrawsBackground(false); |
| 702 | | - unsafe { |
| 703 | | - let _: () = msg_send![&*label, setAlignment: 1_isize]; // NSTextAlignmentCenter |
| 704 | | - } |
| 705 | | - parent.addSubview(&label); |
| 706 | | -} |
| 707 | | - |
| 708 | | -// --- Segment helper --- |
| 709 | | - |
| 710 | | -fn set_segment_label(seg: &NSSegmentedControl, index: isize, label: &str) { |
| 711 | | - let ns = NSString::from_str(label); |
| 712 | | - seg.setLabel_forSegment(&ns, index); |
| 713 | | -} |
| 714 | | - |
| 715 | | -// --- Shared control helpers --- |
| 716 | | - |
| 717 | | -fn add_section_label(mtm: MainThreadMarker, parent: &NSView, text: &str, x: f64, y: f64) { |
| 718 | | - let frame = CGRect::new(CGPoint::new(x, y), CGSize::new(360.0, 20.0)); |
| 719 | | - let label: Retained<NSTextField> = |
| 720 | | - unsafe { msg_send![NSTextField::alloc(mtm), initWithFrame: frame] }; |
| 721 | | - label.setStringValue(&NSString::from_str(text)); |
| 722 | | - label.setEditable(false); |
| 723 | | - label.setBordered(false); |
| 724 | | - label.setDrawsBackground(false); |
| 725 | | - unsafe { |
| 726 | | - let bold: Retained<objc2_app_kit::NSFont> = |
| 727 | | - msg_send![objc2_app_kit::NSFont::class(), boldSystemFontOfSize: 13.0_f64]; |
| 728 | | - label.setFont(Some(&bold)); |
| 729 | | - } |
| 730 | | - parent.addSubview(&label); |
| 731 | | -} |
| 732 | | - |
| 733 | | -fn add_label(mtm: MainThreadMarker, parent: &NSView, text: &str, x: f64, y: f64) { |
| 734 | | - let frame = CGRect::new(CGPoint::new(x, y + 2.0), CGSize::new(150.0, 18.0)); |
| 735 | | - let label: Retained<NSTextField> = |
| 736 | | - unsafe { msg_send![NSTextField::alloc(mtm), initWithFrame: frame] }; |
| 737 | | - label.setStringValue(&NSString::from_str(text)); |
| 738 | | - label.setEditable(false); |
| 739 | | - label.setBordered(false); |
| 740 | | - label.setDrawsBackground(false); |
| 741 | | - parent.addSubview(&label); |
| 742 | | -} |
| 743 | | - |
| 744 | | -fn add_value_label( |
| 745 | | - mtm: MainThreadMarker, |
| 746 | | - parent: &NSView, |
| 747 | | - text: &str, |
| 748 | | - x: f64, |
| 749 | | - y: f64, |
| 750 | | -) -> Retained<NSTextField> { |
| 751 | | - let frame = CGRect::new(CGPoint::new(x, y + 2.0), CGSize::new(30.0, 18.0)); |
| 752 | | - let label: Retained<NSTextField> = |
| 753 | | - unsafe { msg_send![NSTextField::alloc(mtm), initWithFrame: frame] }; |
| 754 | | - label.setStringValue(&NSString::from_str(text)); |
| 755 | | - label.setEditable(false); |
| 756 | | - label.setBordered(false); |
| 757 | | - label.setDrawsBackground(false); |
| 758 | | - parent.addSubview(&label); |
| 759 | | - label |
| 760 | | -} |
| 761 | | - |
| 762 | | -fn set_value_label(label: &NSTextField, val: f64) { |
| 763 | | - let ns = NSString::from_str(&format!("{}", val as i32)); |
| 764 | | - label.setStringValue(&ns); |
| 765 | | -} |
| 766 | | - |
| 767 | | -#[allow(clippy::too_many_arguments)] |
| 768 | | -fn add_slider( |
| 769 | | - mtm: MainThreadMarker, |
| 770 | | - parent: &NSView, |
| 771 | | - handler: &SettingsHandler, |
| 772 | | - label_text: &str, |
| 773 | | - label_x: f64, |
| 774 | | - control_x: f64, |
| 775 | | - y: f64, |
| 776 | | - width: f64, |
| 777 | | - min: f64, |
| 778 | | - max: f64, |
| 779 | | - initial: f64, |
| 780 | | - action: objc2::runtime::Sel, |
| 781 | | -) -> Retained<NSSlider> { |
| 782 | | - add_label(mtm, parent, label_text, label_x, y); |
| 783 | | - let frame = CGRect::new(CGPoint::new(control_x, y), CGSize::new(width, 20.0)); |
| 784 | | - let slider: Retained<NSSlider> = |
| 785 | | - unsafe { msg_send![NSSlider::alloc(mtm), initWithFrame: frame] }; |
| 786 | | - slider.setMinValue(min); |
| 787 | | - slider.setMaxValue(max); |
| 788 | | - slider.setDoubleValue(initial); |
| 789 | | - unsafe { |
| 790 | | - slider.setTarget(Some(handler)); |
| 791 | | - slider.setAction(Some(action)); |
| 792 | | - slider.setContinuous(true); |
| 793 | | - } |
| 794 | | - parent.addSubview(&slider); |
| 795 | | - slider |
| 796 | | -} |
| 797 | | - |
| 798 | | -fn add_color_well( |
| 799 | | - mtm: MainThreadMarker, |
| 800 | | - parent: &NSView, |
| 801 | | - handler: &SettingsHandler, |
| 802 | | - x: f64, |
| 803 | | - y: f64, |
| 804 | | - action: objc2::runtime::Sel, |
| 805 | | -) -> Retained<NSColorWell> { |
| 806 | | - let frame = CGRect::new(CGPoint::new(x, y), CGSize::new(44.0, 24.0)); |
| 807 | | - let well: Retained<NSColorWell> = |
| 808 | | - unsafe { msg_send![NSColorWell::alloc(mtm), initWithFrame: frame] }; |
| 809 | | - unsafe { |
| 810 | | - well.setTarget(Some(handler)); |
| 811 | | - well.setAction(Some(action)); |
| 812 | | - } |
| 813 | | - parent.addSubview(&well); |
| 814 | | - well |
| 815 | | -} |
| 816 | | - |
| 817 | | -fn add_checkbox( |
| 818 | | - mtm: MainThreadMarker, |
| 819 | | - parent: &NSView, |
| 820 | | - handler: &SettingsHandler, |
| 821 | | - title: &str, |
| 822 | | - x: f64, |
| 823 | | - y: f64, |
| 824 | | - action: objc2::runtime::Sel, |
| 825 | | -) -> Retained<NSButton> { |
| 826 | | - let frame = CGRect::new(CGPoint::new(x, y), CGSize::new(250.0, 22.0)); |
| 827 | | - let btn: Retained<NSButton> = unsafe { msg_send![NSButton::alloc(mtm), initWithFrame: frame] }; |
| 828 | | - btn.setTitle(&NSString::from_str(title)); |
| 829 | | - unsafe { |
| 830 | | - let _: () = msg_send![&*btn, setButtonType: 3_isize]; // NSSwitchButton |
| 831 | | - btn.setTarget(Some(handler)); |
| 832 | | - btn.setAction(Some(action)); |
| 833 | | - } |
| 834 | | - parent.addSubview(&btn); |
| 835 | | - btn |
| 836 | | -} |
| 837 | | - |
| 838 | | -fn add_popup( |
| 839 | | - mtm: MainThreadMarker, |
| 840 | | - parent: &NSView, |
| 841 | | - handler: &SettingsHandler, |
| 842 | | - x: f64, |
| 843 | | - y: f64, |
| 844 | | - width: f64, |
| 845 | | - items: &[&str], |
| 846 | | - action: objc2::runtime::Sel, |
| 847 | | -) -> Retained<NSPopUpButton> { |
| 848 | | - let frame = CGRect::new(CGPoint::new(x, y), CGSize::new(width, 26.0)); |
| 849 | | - let popup: Retained<NSPopUpButton> = |
| 850 | | - unsafe { msg_send![NSPopUpButton::alloc(mtm), initWithFrame: frame, pullsDown: false] }; |
| 851 | | - for item in items { |
| 852 | | - popup.addItemWithTitle(&NSString::from_str(item)); |
| 853 | | - } |
| 854 | | - unsafe { |
| 855 | | - popup.setTarget(Some(handler)); |
| 856 | | - popup.setAction(Some(action)); |
| 857 | | - } |
| 858 | | - parent.addSubview(&popup); |
| 859 | | - popup |
| 860 | | -} |
| 861 | | - |
| 862 | | -fn color_well_to_hex(well: &NSColorWell) -> String { |
| 863 | | - unsafe { |
| 864 | | - let color = well.color(); |
| 865 | | - let rgb: Option<Retained<objc2_app_kit::NSColor>> = msg_send![ |
| 866 | | - &*color, |
| 867 | | - colorUsingColorSpaceName: &*NSString::from_str("NSCalibratedRGBColorSpace") |
| 868 | | - ]; |
| 869 | | - if let Some(rgb) = rgb { |
| 870 | | - let r: f64 = msg_send![&*rgb, redComponent]; |
| 871 | | - let g: f64 = msg_send![&*rgb, greenComponent]; |
| 872 | | - let b: f64 = msg_send![&*rgb, blueComponent]; |
| 873 | | - format!( |
| 874 | | - "#{:02x}{:02x}{:02x}", |
| 875 | | - (r * 255.0) as u8, |
| 876 | | - (g * 255.0) as u8, |
| 877 | | - (b * 255.0) as u8 |
| 878 | | - ) |
| 879 | | - } else { |
| 880 | | - "#000000".to_string() |
| 881 | | - } |
| 882 | | - } |
| 883 | | -} |
| 884 | | - |
| 885 | | -fn set_color_well(well: &NSColorWell, hex: &str) { |
| 886 | | - let hex = hex.trim_start_matches('#'); |
| 887 | | - if hex.len() < 6 { |
| 888 | | - return; |
| 889 | | - } |
| 890 | | - let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0) as f64 / 255.0; |
| 891 | | - let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0) as f64 / 255.0; |
| 892 | | - let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(0) as f64 / 255.0; |
| 893 | | - unsafe { |
| 894 | | - let color: Retained<objc2_app_kit::NSColor> = msg_send![ |
| 895 | | - objc2_app_kit::NSColor::class(), |
| 896 | | - colorWithCalibratedRed: r, green: g, blue: b, alpha: 1.0_f64 |
| 897 | | - ]; |
| 898 | | - well.setColor(&color); |
| 899 | | - } |
| 900 | | -} |