diff --git a/src/glyph_shader.wgsl b/src/glyph_shader.wgsl index eb33c7d..a1faf16 100644 --- a/src/glyph_shader.wgsl +++ b/src/glyph_shader.wgsl @@ -459,11 +459,16 @@ fn vs_cell_bg( // For default background (type 0), use fully transparent so the window's // clear color (which has background_opacity applied) shows through. + // UNLESS the grid params specify an opaque background (e.g. alternate screen). // Only non-default backgrounds should be opaque. // But NOT if the cell is selected (selection always has white bg) let bg_type = cell.bg & 0xFFu; if bg_type == COLOR_TYPE_DEFAULT && !is_reverse && !is_selected { - bg.a = 0.0; + if grid_params.background_opacity < 1.0 { + bg.a = 0.0; + } else { + bg.a = 1.0; + } } // Calculate cursor color diff --git a/src/main.rs b/src/main.rs index 08d721d..9d895cd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -879,7 +879,7 @@ fn remove_pid_file() { /// - Last segment is bold /// - Section has a dark gray background color (#282828) /// - Section ends with powerline arrow transition -fn build_cwd_section(cwd: &str) -> StatuslineSection { +fn build_cwd_section(cwd: &str, is_light: bool) -> StatuslineSection { // Colors to cycle through (skip 0 and 1 which are often near-white in custom schemes) const COLORS: [u8; 6] = [2, 3, 4, 5, 6, 7]; @@ -903,7 +903,12 @@ fn build_cwd_section(cwd: &str) -> StatuslineSection { if segments.is_empty() { // Root directory components.push(StatuslineComponent::new(" \u{F07C} / ").fg(COLORS[0])); - return StatuslineSection::with_rgb_bg(0x28, 0x28, 0x28) + let (bg_r, bg_g, bg_b) = if is_light { + (0xE0, 0xE0, 0xE0) + } else { + (0x28, 0x28, 0x28) + }; + return StatuslineSection::with_rgb_bg(bg_r, bg_g, bg_b) .with_components(components); } @@ -940,8 +945,12 @@ fn build_cwd_section(cwd: &str) -> StatuslineSection { // Add trailing space for padding before the powerline arrow components.push(StatuslineComponent::new(" ")); - // Use dark gray (#282828) as section background - StatuslineSection::with_rgb_bg(0x28, 0x28, 0x28).with_components(components) + let (bg_r, bg_g, bg_b) = if is_light { + (0xE0, 0xE0, 0xE0) + } else { + (0x28, 0x28, 0x28) + }; + StatuslineSection::with_rgb_bg(bg_r, bg_g, bg_b).with_components(components) } /// Git repository status information. @@ -1101,7 +1110,7 @@ fn get_git_status(cwd: &str) -> Option { /// Build a statusline section for git status. /// Returns None if not in a git repository. -fn build_git_section(cwd: &str) -> Option { +fn build_git_section(cwd: &str, is_light: bool) -> Option { let status = get_git_status(cwd)?; // Determine foreground color based on state (matching oh-my-posh template) @@ -1111,13 +1120,29 @@ fn build_git_section(cwd: &str) -> Option { // 3. If both ahead and behind: #ff4500 (red-orange) // 4. If ahead or behind: #B388FF (purple) let fg_color: (u8, u8, u8) = if status.ahead > 0 && status.behind > 0 { - (0xff, 0x45, 0x00) // #ff4500 - red-orange + if is_light { + (0xCC, 0x37, 0x00) + } else { + (0xff, 0x45, 0x00) + } // red-orange (darker for light mode) } else if status.ahead > 0 || status.behind > 0 { - (0xB3, 0x88, 0xFF) // #B388FF - purple + if is_light { + (0x7B, 0x4C, 0xCC) + } else { + (0xB3, 0x88, 0xFF) + } // purple (darker for light mode) } else if status.working_changed > 0 || status.staging_changed > 0 { - (0xFF, 0x92, 0x48) // #FF9248 - orange + if is_light { + (0xC0, 0x60, 0x10) + } else { + (0xFF, 0x92, 0x48) + } // orange (darker for light mode) } else { - (0x0d, 0xa3, 0x00) // #0da300 - green + if is_light { + (0x0A, 0x7D, 0x00) + } else { + (0x0d, 0xa3, 0x00) + } // green (darker for light mode) }; let mut components = Vec::new(); @@ -1192,9 +1217,15 @@ fn build_git_section(cwd: &str) -> Option { .rgb_fg(fg_color.0, fg_color.1, fg_color.2), ); - // Background: #232323 + // Background + let (bg_r, bg_g, bg_b) = if is_light { + (0xD0, 0xD0, 0xD0) // Light mode background + } else { + (0x23, 0x23, 0x23) // Dark mode background + }; + Some( - StatuslineSection::with_rgb_bg(0x23, 0x23, 0x23) + StatuslineSection::with_rgb_bg(bg_r, bg_g, bg_b) .with_components(components), ) } @@ -1293,6 +1324,8 @@ struct App { shutdown: Arc, /// Current mouse cursor position. cursor_position: PhysicalPosition, + /// Where the mouse was last pressed (for drag selection threshold). + mouse_down_pos: Option>, /// Frame counter for FPS logging. frame_count: u64, /// Last time we logged FPS. @@ -1345,6 +1378,7 @@ impl App { event_loop_proxy: None, shutdown: Arc::new(AtomicBool::new(false)), cursor_position: PhysicalPosition::new(0.0, 0.0), + mouse_down_pos: None, frame_count: 0, last_frame_log: std::time::Instant::now(), should_create_window: false, @@ -2019,8 +2053,12 @@ impl App { if let Some(ref custom) = pane.custom_statusline { StatuslineContent::Raw(custom.clone()) } else if let Some(cwd) = pane.pty.foreground_cwd() { - let mut sections = vec![build_cwd_section(&cwd)]; - if let Some(git_section) = build_git_section(&cwd) { + let is_light = pane.terminal.palette.is_light(); + let mut sections = + vec![build_cwd_section(&cwd, is_light)]; + if let Some(git_section) = + build_git_section(&cwd, is_light) + { sections.push(git_section); } StatuslineContent::Sections(sections) @@ -2980,6 +3018,63 @@ impl ApplicationHandler for App { } } } + } else if let Some(down_pos) = self.mouse_down_pos { + if !self.has_mouse_tracking() { + let dx = position.x - down_pos.x; + let dy = position.y - down_pos.y; + let distance_sq = dx * dx + dy * dy; + + let cell_width = self + .renderer + .as_ref() + .map(|r| r.cell_metrics.cell_width as f64) + .unwrap_or(8.0); + let threshold = cell_width * 0.5; + + if distance_sq > threshold * threshold { + // Dragged far enough, start selection + if let Some(renderer) = &self.renderer { + if let Some((start_col, start_screen_row)) = + renderer + .pixel_to_cell(down_pos.x, down_pos.y) + { + if let Some((end_col, end_screen_row)) = + renderer.pixel_to_cell( + position.x, position.y, + ) + { + let scroll_offset = + self.get_scroll_offset() as isize; + let start_pos = CellPosition { + col: start_col, + row: start_screen_row as isize + - scroll_offset, + }; + let end_pos = CellPosition { + col: end_col, + row: end_screen_row as isize + - scroll_offset, + }; + + if let Some(tab) = self.active_tab_mut() + { + if let Some(pane) = + tab.active_pane_mut() + { + pane.selection = + Some(Selection { + start: start_pos, + end: end_pos, + }); + pane.is_selecting = true; + self.request_redraw(); + } + } + } + } + } + } + } } } @@ -3022,37 +3117,16 @@ impl ApplicationHandler for App { } else if button == MouseButton::Left { match state { ElementState::Pressed => { - if let Some(renderer) = &self.renderer { - if let Some((col, screen_row)) = renderer - .pixel_to_cell( - self.cursor_position.x, - self.cursor_position.y, - ) - { - let scroll_offset = - self.get_scroll_offset(); - let content_row = screen_row as isize - - scroll_offset as isize; - let pos = CellPosition { - col, - row: content_row, - }; - log::debug!("Selection started at col={}, content_row={}, screen_row={}, scroll_offset={}", col, content_row, screen_row, scroll_offset); - if let Some(tab) = self.active_tab_mut() { - if let Some(pane) = - tab.active_pane_mut() - { - pane.selection = Some(Selection { - start: pos, - end: pos, - }); - pane.is_selecting = true; - } - } + self.mouse_down_pos = Some(self.cursor_position); + if let Some(tab) = self.active_tab_mut() { + if let Some(pane) = tab.active_pane_mut() { + pane.selection = None; + pane.is_selecting = false; } } } ElementState::Released => { + self.mouse_down_pos = None; let was_selecting = self .active_pane() .map(|p| p.is_selecting) diff --git a/src/renderer.rs b/src/renderer.rs index 2a6efd9..59eadf5 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -2406,14 +2406,19 @@ impl Renderer { /// Parse ANSI escape sequences from raw statusline content. /// Returns a vector of (char, fg_color, bg_color, bold) tuples. - fn parse_ansi_statusline(content: &str) -> Vec<(char, StatuslineColor, StatuslineColor, bool)> { + fn parse_ansi_statusline(content: &str, is_light: bool) -> Vec<(char, StatuslineColor, StatuslineColor, bool)> { let mut result = Vec::new(); let chars: Vec = content.chars().collect(); let mut i = 0; // Current styling state let mut fg = StatuslineColor::Default; - let mut bg = StatuslineColor::Rgb(0x1a, 0x1a, 0x1a); // Default statusline background + let default_bg_color = if is_light { + StatuslineColor::Rgb(0xD0, 0xD0, 0xD0) + } else { + StatuslineColor::Rgb(0x1a, 0x1a, 0x1a) + }; + let mut bg = default_bg_color.clone(); // Default statusline background let mut bold = false; while i < chars.len() { @@ -2494,7 +2499,7 @@ impl Renderer { } } } - 49 => bg = StatuslineColor::Rgb(0x1a, 0x1a, 0x1a), // Reset to default statusline bg + 49 => bg = default_bg_color.clone(), // Reset to default statusline bg 90..=97 => fg = StatuslineColor::Indexed((code - 90 + 8) as u8), 100..=107 => bg = StatuslineColor::Indexed((code - 100 + 8) as u8), _ => {} @@ -2528,7 +2533,7 @@ impl Renderer { /// this is used to expand the middle gap to fill the full window width. /// /// Returns the number of columns used. - fn update_statusline_cells(&mut self, content: &StatuslineContent, target_width: f32) -> usize { + fn update_statusline_cells(&mut self, content: &StatuslineContent, target_width: f32, is_light: bool) -> usize { self.statusline_gpu_cells.clear(); // Calculate target columns based on window width @@ -2540,14 +2545,19 @@ impl Renderer { self.statusline_max_cols }; - // Default background color for statusline (dark gray) - let default_bg = Self::pack_statusline_color(StatuslineColor::Rgb(0x1a, 0x1a, 0x1a)); + // Default background color for statusline + let default_bg_color = if is_light { + StatuslineColor::Rgb(0xD0, 0xD0, 0xD0) + } else { + StatuslineColor::Rgb(0x1a, 0x1a, 0x1a) + }; + let default_bg = Self::pack_statusline_color(default_bg_color); let _ = default_bg; // Silence unused warning - used by Sections path match content { StatuslineContent::Raw(ansi_content) => { // Parse ANSI escape sequences to extract colors and text - let parsed = Self::parse_ansi_statusline(ansi_content); + let parsed = Self::parse_ansi_statusline(ansi_content, is_light); // Find the middle gap (largest consecutive run of spaces) // and expand it to fill the target width @@ -2597,7 +2607,7 @@ impl Renderer { let gap_bg = if best_gap_len > 0 && best_gap_start < parsed.len() { parsed[best_gap_start].2 } else { - StatuslineColor::Rgb(0x1a, 0x1a, 0x1a) + default_bg_color.clone() }; // The position right before right-hand content starts (end of gap) @@ -2845,7 +2855,7 @@ impl Renderer { // Fill remaining width with default background cells // This ensures the statusline covers the entire window width - let default_bg_packed = Self::pack_statusline_color(StatuslineColor::Default); + let default_bg_packed = default_bg; while self.statusline_gpu_cells.len() < target_cols && self.statusline_gpu_cells.len() < self.statusline_max_cols { self.statusline_gpu_cells.push(GPUCell { fg: 0, @@ -4331,10 +4341,17 @@ impl Renderer { TabBarPosition::Hidden => unreachable!(), }; - // Use same color as statusline: 0x1a1a1a (26, 26, 26) in sRGB - // Pre-computed linear RGB value for srgb_to_linear(26/255) ≈ 0.00972 - const TAB_BAR_BG_LINEAR: f32 = 0.00972; - let tab_bar_bg = [TAB_BAR_BG_LINEAR, TAB_BAR_BG_LINEAR, TAB_BAR_BG_LINEAR, 1.0]; + let is_light = self.palette.is_light(); + let tab_bar_bg = if is_light { + // Light mode statusline bg is approx 0xD0, linear is ~0.63076 + const TAB_BAR_BG_LINEAR_LIGHT: f32 = 0.63076; + [TAB_BAR_BG_LINEAR_LIGHT, TAB_BAR_BG_LINEAR_LIGHT, TAB_BAR_BG_LINEAR_LIGHT, 1.0] + } else { + // Use same color as statusline: 0x1a1a1a (26, 26, 26) in sRGB + // Pre-computed linear RGB value for srgb_to_linear(26/255) ≈ 0.00972 + const TAB_BAR_BG_LINEAR_DARK: f32 = 0.00972; + [TAB_BAR_BG_LINEAR_DARK, TAB_BAR_BG_LINEAR_DARK, TAB_BAR_BG_LINEAR_DARK, 1.0] + }; // Draw tab bar background log::debug!("render_panes: drawing tab bar at y={}, height={}, num_tabs={}, quads_before={}", @@ -4354,23 +4371,23 @@ impl Renderer { let tab_width = title_width.max(min_tab_width); let tab_bg = if is_active { - // Active tab: brightest - significantly brighter than tab bar + // Active tab: brightest - matches terminal background or slightly brighter let [r, g, b] = self.palette.default_bg; - let boost = 50.0_f32; // More visible for active tab + let boost = if is_light { 0.0_f32 } else { 50.0_f32 }; [ - Self::srgb_to_linear((r as f32 + boost).min(255.0) / 255.0), - Self::srgb_to_linear((g as f32 + boost).min(255.0) / 255.0), - Self::srgb_to_linear((b as f32 + boost).min(255.0) / 255.0), + Self::srgb_to_linear((r as f32 + boost).clamp(0.0, 255.0) / 255.0), + Self::srgb_to_linear((g as f32 + boost).clamp(0.0, 255.0) / 255.0), + Self::srgb_to_linear((b as f32 + boost).clamp(0.0, 255.0) / 255.0), 1.0, ] } else { - // Inactive tab: slightly brighter than tab bar background + // Inactive tab: between tab bar background and active tab let [r, g, b] = self.palette.default_bg; - let boost = 30.0_f32; + let boost = if is_light { -30.0_f32 } else { 30.0_f32 }; [ - Self::srgb_to_linear((r as f32 + boost).min(255.0) / 255.0), - Self::srgb_to_linear((g as f32 + boost).min(255.0) / 255.0), - Self::srgb_to_linear((b as f32 + boost).min(255.0) / 255.0), + Self::srgb_to_linear((r as f32 + boost).clamp(0.0, 255.0) / 255.0), + Self::srgb_to_linear((g as f32 + boost).clamp(0.0, 255.0) / 255.0), + Self::srgb_to_linear((b as f32 + boost).clamp(0.0, 255.0) / 255.0), 1.0, ] }; @@ -4651,7 +4668,11 @@ impl Renderer { CursorShape::BlinkingUnderline | CursorShape::SteadyUnderline => 1, CursorShape::BlinkingBar | CursorShape::SteadyBar => 2, }, - background_opacity: self.background_opacity, + background_opacity: if terminal.using_alternate_screen { + 1.0 + } else { + self.background_opacity + }, selection_start_col: sel_start_col, selection_start_row: sel_start_row, selection_end_col: sel_end_col, @@ -4767,9 +4788,10 @@ impl Renderer { // ═══════════════════════════════════════════════════════════════════ let statusline_cols = { let statusline_y = self.statusline_y(); + let is_light = self.palette.is_light(); // Update statusline GPU cells from content, passing window width for gap expansion - let cols = self.update_statusline_cells(statusline_content, width); + let cols = self.update_statusline_cells(statusline_content, width, is_light); if cols > 0 { // Upload statusline cells to GPU @@ -5009,10 +5031,19 @@ impl Renderer { { let [bg_r, bg_g, bg_b] = self.palette.default_bg; - let bg_r_linear = Self::srgb_to_linear(bg_r as f32 / 255.0) as f64; - let bg_g_linear = Self::srgb_to_linear(bg_g as f32 / 255.0) as f64; - let bg_b_linear = Self::srgb_to_linear(bg_b as f32 / 255.0) as f64; + let mut bg_r_linear = Self::srgb_to_linear(bg_r as f32 / 255.0) as f64; + let mut bg_g_linear = Self::srgb_to_linear(bg_g as f32 / 255.0) as f64; + let mut bg_b_linear = Self::srgb_to_linear(bg_b as f32 / 255.0) as f64; let bg_alpha = self.background_opacity as f64; + + // If the compositor expects premultiplied alpha, we must premultiply the clear color. + // Otherwise, light backgrounds with opacity will look fully opaque or super-luminous. + if self.surface_config.alpha_mode == wgpu::CompositeAlphaMode::PreMultiplied { + bg_r_linear *= bg_alpha; + bg_g_linear *= bg_alpha; + bg_b_linear *= bg_alpha; + } + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("Render Pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { diff --git a/src/terminal.rs b/src/terminal.rs index 07d6a86..050513c 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -186,6 +186,15 @@ impl Default for ColorPalette { } impl ColorPalette { + /// Return whether this palette is considered "light" based on default background luminance. + pub fn is_light(&self) -> bool { + let [r, g, b] = self.default_bg; + // Standard perceived luminance calculation + let luminance = + 0.299 * (r as f32) + 0.587 * (g as f32) + 0.114 * (b as f32); + luminance > 128.0 + } + /// Parse a color specification like "#RRGGBB" or "rgb:RR/GG/BB". pub fn parse_color_spec(spec: &str) -> Option<[u8; 3]> { let spec = spec.trim();