refactor: read show_etd from state for merge compatibility

Revert shared window.rs draw/width signatures to base; read show_etd via a
state-lock accessor and share a lock-free text-width slot mechanism with the
detailed-remaining feature (identical block, distinct per-feature wiring).
Place ETD's struct fields, menu ID, and Settings menu item at anchors that are
non-adjacent to the other display features so any merge subset is conflict-free.
Keeps poller's format_countdown_from_secs at its base 2-arg signature.
This commit is contained in:
Carlos Miguel C. Resurreccion 2026-06-19 20:17:05 +08:00
parent 20c44dbb9e
commit ddee8d9f9b

View File

@ -1,5 +1,5 @@
use std::path::PathBuf;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::atomic::{AtomicI32, AtomicU32, Ordering};
use std::sync::{Mutex, MutexGuard};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
@ -65,7 +65,6 @@ struct AppState {
codex_weekly_text: String,
show_claude_code: bool,
show_codex: bool,
show_etd: bool,
data: Option<AppUsageData>,
@ -85,6 +84,7 @@ struct AppState {
drag_start_offset: i32,
widget_visible: bool,
show_etd: bool,
}
#[derive(Clone, Debug)]
@ -123,8 +123,8 @@ const IDM_LANG_TRADITIONAL_CHINESE: u16 = 48;
const IDM_LANG_RUSSIAN: u16 = 49;
const IDM_LANG_PORTUGUESE_BRAZIL: u16 = 50;
const IDM_MODEL_CLAUDE_CODE: u16 = 60;
const IDM_MODEL_CODEX: u16 = 61;
const IDM_SHOW_ETD: u16 = 74;
const IDM_MODEL_CODEX: u16 = 61;
const DIVIDER_HIT_ZONE: i32 = 13; // LEFT_DIVIDER_W + DIVIDER_RIGHT_MARGIN
@ -213,12 +213,12 @@ struct SettingsFile {
last_update_check_unix: Option<u64>,
#[serde(default = "default_widget_visible")]
widget_visible: bool,
#[serde(default)]
show_etd: bool,
#[serde(default = "default_show_claude_code")]
show_claude_code: bool,
#[serde(default = "default_show_codex")]
show_codex: bool,
#[serde(default)]
show_etd: bool,
}
impl Default for SettingsFile {
@ -229,9 +229,9 @@ impl Default for SettingsFile {
language: None,
last_update_check_unix: None,
widget_visible: true,
show_etd: false,
show_claude_code: true,
show_codex: false,
show_etd: false,
}
}
}
@ -285,9 +285,9 @@ fn save_state_settings() {
.map(|language| language.code().to_string()),
last_update_check_unix: s.last_update_check_unix,
widget_visible: s.widget_visible,
show_etd: s.show_etd,
show_claude_code: s.show_claude_code,
show_codex: s.show_codex,
show_etd: s.show_etd,
});
}
}
@ -847,27 +847,45 @@ const DIVIDER_RIGHT_MARGIN: i32 = 10;
const LABEL_WIDTH: i32 = 18;
const LABEL_RIGHT_MARGIN: i32 = 10;
const BAR_RIGHT_MARGIN: i32 = 4;
const TEXT_WIDTH: i32 = 62; // fits the worst-case "100% · 59m"
const TEXT_WIDTH: i32 = 62;
const MODEL_RIGHT_MARGIN: i32 = 5;
const RIGHT_MARGIN: i32 = 1;
const WIDGET_HEIGHT: i32 = 46;
/// Text column when the ETD suffix is shown. The ETD form is worst-case
/// "100% · 59m rem · 59m ETD" — the base "100% · 59m" plus a " rem · 59m ETD"
/// label-and-estimate of similar length — so the column is sized to fit the
/// widest possible remaining-time and ETD (~2.6x the base) without clipping.
const ETD_TEXT_WIDTH: i32 = 160;
const MODEL_RIGHT_MARGIN: i32 = 5;
const RIGHT_MARGIN: i32 = 1;
const WIDGET_HEIGHT: i32 = 46;
fn active_model_count(show_claude_code: bool, show_codex: bool) -> i32 {
(show_claude_code as i32 + show_codex as i32).max(1)
}
/// Text-column width for a usage cell, widened when the ETD suffix is shown.
fn text_width(show_etd: bool) -> i32 {
if show_etd {
ETD_TEXT_WIDTH
} else {
TEXT_WIDTH
}
// Optional display features may widen the usage text column. Each feature
// stores its own extra width (in base 96-DPI px) in a dedicated slot, read
// lock-free by the width/layout helpers below. Slots are summed so multiple
// features stack. This block is identical across the display-feature branches
// so they merge without conflict; a branch only wires up its own slot.
#[allow(dead_code)]
static EXTRA_TEXT_WIDTH_DETAILED: AtomicI32 = AtomicI32::new(0);
#[allow(dead_code)]
static EXTRA_TEXT_WIDTH_ETD: AtomicI32 = AtomicI32::new(0);
#[allow(dead_code)]
fn set_extra_text_width_detailed(px: i32) {
EXTRA_TEXT_WIDTH_DETAILED.store(px, Ordering::Relaxed);
}
#[allow(dead_code)]
fn set_extra_text_width_etd(px: i32) {
EXTRA_TEXT_WIDTH_ETD.store(px, Ordering::Relaxed);
}
/// Usage text-column width including any enabled display-feature widening.
fn effective_text_width() -> i32 {
TEXT_WIDTH
+ EXTRA_TEXT_WIDTH_DETAILED.load(Ordering::Relaxed)
+ EXTRA_TEXT_WIDTH_ETD.load(Ordering::Relaxed)
}
fn row_bar_segment_count(active_models: i32) -> i32 {
@ -878,11 +896,11 @@ fn row_bar_segment_count(active_models: i32) -> i32 {
}
}
fn total_widget_width_for(active_models: i32, show_etd: bool) -> i32 {
fn total_widget_width_for(active_models: i32) -> i32 {
let bar_segments = row_bar_segment_count(active_models);
let model_width = (sc(SEGMENT_W) + sc(SEGMENT_GAP)) * bar_segments - sc(SEGMENT_GAP)
+ sc(BAR_RIGHT_MARGIN)
+ sc(text_width(show_etd));
+ sc(effective_text_width());
sc(LEFT_DIVIDER_W)
+ sc(DIVIDER_RIGHT_MARGIN)
@ -893,22 +911,22 @@ fn total_widget_width_for(active_models: i32, show_etd: bool) -> i32 {
+ sc(RIGHT_MARGIN)
}
fn total_widget_width_for_state(state: &AppState) -> i32 {
total_widget_width_for(
active_model_count(state.show_claude_code, state.show_codex),
state.show_etd,
)
}
fn total_widget_width() -> i32 {
let (active_models, show_etd) = {
let active_models = {
let state = lock_state();
state
.as_ref()
.map(|s| (active_model_count(s.show_claude_code, s.show_codex), s.show_etd))
.unwrap_or((1, false))
.map(|s| active_model_count(s.show_claude_code, s.show_codex))
.unwrap_or(1)
};
total_widget_width_for(active_models, show_etd)
total_widget_width_for(active_models)
}
/// Whether the ETD suffix is enabled, read from shared state. Returns false
/// when state is not yet populated (startup) or the lock cannot be acquired.
/// Callers must not hold the state lock.
fn show_etd_enabled() -> bool {
lock_state().as_ref().map_or(false, |s| s.show_etd)
}
fn claude_accent_color() -> Color {
@ -1009,7 +1027,7 @@ pub fn run() {
WS_POPUP,
0,
0,
total_widget_width_for(initial_model_count, settings.show_etd),
total_widget_width_for(initial_model_count),
sc(WIDGET_HEIGHT),
HWND::default(),
HMENU::default(),
@ -1062,7 +1080,6 @@ pub fn run() {
codex_weekly_text: "--".to_string(),
show_claude_code: settings.show_claude_code,
show_codex: settings.show_codex,
show_etd: settings.show_etd,
data: None,
poll_interval_ms: settings.poll_interval_ms,
retry_count: 0,
@ -1078,6 +1095,7 @@ pub fn run() {
drag_start_mouse_x: 0,
drag_start_offset: 0,
widget_visible: settings.widget_visible,
show_etd: settings.show_etd,
});
}
@ -1114,6 +1132,14 @@ pub fn run() {
diagnose::log("taskbar not found; using fallback popup window");
}
// Seed the ETD text-column widening so the initial layout matches the
// wider draw rect when the ETD suffix is enabled at startup.
set_extra_text_width_etd(if settings.show_etd {
ETD_TEXT_WIDTH - TEXT_WIDTH
} else {
0
});
// If not embedded, fall back to topmost popup with SetLayeredWindowAttributes
if !embedded {
let _ = SetLayeredWindowAttributes(hwnd, COLORREF(0), 255, LWA_ALPHA);
@ -1202,7 +1228,6 @@ fn render_layered() {
codex_weekly_text,
show_claude_code,
show_codex,
show_etd,
) = {
let state = lock_state();
match state.as_ref() {
@ -1221,7 +1246,6 @@ fn render_layered() {
s.codex_weekly_text.clone(),
s.show_claude_code,
s.show_codex,
s.show_etd,
),
None => return,
}
@ -1311,7 +1335,6 @@ fn render_layered() {
&codex_weekly_text,
show_claude_code,
show_codex,
show_etd,
&codex_accent,
);
@ -1382,7 +1405,6 @@ fn paint_content(
codex_weekly_text: &str,
show_claude_code: bool,
show_codex: bool,
show_etd: bool,
codex_accent: &Color,
) {
unsafe {
@ -1473,7 +1495,6 @@ fn paint_content(
codex_session_text,
show_claude_code,
show_codex,
show_etd,
accent,
codex_accent,
track,
@ -1491,7 +1512,6 @@ fn paint_content(
codex_weekly_text,
show_claude_code,
show_codex,
show_etd,
accent,
codex_accent,
track,
@ -2068,6 +2088,9 @@ unsafe extern "system" fn wnd_proc(
if is_dragging {
let mut pt = POINT::default();
let _ = GetCursorPos(&mut pt);
// Compute the widget width before locking; the width helpers read
// shared state themselves and must not run while the lock is held.
let widget_width = total_widget_width();
let move_target = {
let mut state = lock_state();
let s = match state.as_mut() {
@ -2101,7 +2124,6 @@ unsafe extern "system" fn wnd_proc(
tray_left = tray_rect.left;
}
}
let widget_width = total_widget_width_for_state(s);
let max_offset = (tray_left - taskbar_rect.left - widget_width).max(0);
if new_offset > max_offset {
new_offset = max_offset;
@ -2311,10 +2333,16 @@ unsafe extern "system" fn wnd_proc(
let mut state = lock_state();
if let Some(s) = state.as_mut() {
s.show_etd = !s.show_etd;
set_extra_text_width_etd(if s.show_etd {
ETD_TEXT_WIDTH - TEXT_WIDTH
} else {
0
});
refresh_usage_texts(s);
}
}
save_state_settings();
position_at_taskbar();
render_layered();
}
IDM_LANG_SYSTEM
@ -2398,7 +2426,6 @@ fn show_context_menu(hwnd: HWND) {
widget_visible,
show_claude_code,
show_codex,
show_etd,
) = {
let state = lock_state();
match state.as_ref() {
@ -2412,7 +2439,6 @@ fn show_context_menu(hwnd: HWND) {
s.widget_visible,
s.show_claude_code,
s.show_codex,
s.show_etd,
),
None => (
POLL_15_MIN,
@ -2424,7 +2450,6 @@ fn show_context_menu(hwnd: HWND) {
true,
true,
false,
false,
),
}
};
@ -2530,19 +2555,6 @@ fn show_context_menu(hwnd: HWND) {
PCWSTR::from_raw(reset_pos_str.as_ptr()),
);
let etd_str = native_interop::wide_str(strings.show_etd);
let etd_flags = if show_etd {
MF_CHECKED
} else {
MENU_ITEM_FLAGS(0)
};
let _ = AppendMenuW(
settings_menu,
etd_flags,
IDM_SHOW_ETD as usize,
PCWSTR::from_raw(etd_str.as_ptr()),
);
let language_menu = CreatePopupMenu().unwrap();
let system_label = native_interop::wide_str(strings.system_default);
let system_flags = if language_override.is_none() {
@ -2592,6 +2604,19 @@ fn show_context_menu(hwnd: HWND) {
PCWSTR::from_raw(language_label.as_ptr()),
);
let etd_str = native_interop::wide_str(strings.show_etd);
let etd_flags = if show_etd_enabled() {
MF_CHECKED
} else {
MENU_ITEM_FLAGS(0)
};
let _ = AppendMenuW(
settings_menu,
etd_flags,
IDM_SHOW_ETD as usize,
PCWSTR::from_raw(etd_str.as_ptr()),
);
let _ = AppendMenuW(settings_menu, MF_SEPARATOR, 0, PCWSTR::null());
let version_label =
@ -2666,7 +2691,6 @@ fn paint(hdc: HDC, hwnd: HWND) {
codex_weekly_text,
show_claude_code,
show_codex,
show_etd,
) = {
let state = lock_state();
match state.as_ref() {
@ -2683,7 +2707,6 @@ fn paint(hdc: HDC, hwnd: HWND) {
s.codex_weekly_text.clone(),
s.show_claude_code,
s.show_codex,
s.show_etd,
),
None => return,
}
@ -2741,7 +2764,6 @@ fn paint(hdc: HDC, hwnd: HWND) {
&codex_weekly_text,
show_claude_code,
show_codex,
show_etd,
&codex_accent,
);
@ -2766,7 +2788,6 @@ fn draw_row(
codex_text: &str,
show_claude_code: bool,
show_codex: bool,
show_etd: bool,
claude_accent: &Color,
codex_accent: &Color,
track: &Color,
@ -2814,9 +2835,8 @@ fn draw_row(
claude_accent,
track,
&claude_value_color,
show_etd,
);
model_x += model_usage_width(segment_count, show_etd) + sc(MODEL_RIGHT_MARGIN);
model_x += model_usage_width(segment_count) + sc(MODEL_RIGHT_MARGIN);
}
if show_codex {
draw_usage_bar(
@ -2829,16 +2849,15 @@ fn draw_row(
codex_accent,
track,
&codex_value_color,
show_etd,
);
}
}
}
fn model_usage_width(segment_count: i32, show_etd: bool) -> i32 {
fn model_usage_width(segment_count: i32) -> i32 {
(sc(SEGMENT_W) + sc(SEGMENT_GAP)) * segment_count - sc(SEGMENT_GAP)
+ sc(BAR_RIGHT_MARGIN)
+ sc(text_width(show_etd))
+ sc(effective_text_width())
}
fn draw_usage_bar(
@ -2851,7 +2870,6 @@ fn draw_usage_bar(
accent: &Color,
track: &Color,
text_color: &Color,
show_etd: bool,
) {
let seg_w = sc(SEGMENT_W);
let seg_h = sc(SEGMENT_H);
@ -2912,7 +2930,7 @@ fn draw_usage_bar(
let mut text_rect = RECT {
left: text_x,
top: y,
right: text_x + sc(text_width(show_etd)),
right: text_x + sc(effective_text_width()),
bottom: y + seg_h,
};
let _ = SetTextColor(hdc, COLORREF(text_color.to_colorref()));
@ -2950,20 +2968,15 @@ mod tests {
fn etd_text_column_is_wider_than_base() {
// Sized for the worst case "100% · 59m rem · 59m ETD".
assert!(ETD_TEXT_WIDTH > TEXT_WIDTH);
assert_eq!(text_width(false), TEXT_WIDTH);
assert_eq!(text_width(true), ETD_TEXT_WIDTH);
}
#[test]
fn widget_widens_by_text_delta_per_model_when_etd_on() {
// Enabling ETD must widen the widget by exactly the column growth, once
// per active model — so the layout calc matches the wider draw rect and
// nothing re-clips.
for models in 1..=2 {
let off = total_widget_width_for(models, false);
let on = total_widget_width_for(models, true);
let expected = (sc(ETD_TEXT_WIDTH) - sc(TEXT_WIDTH)) * models;
assert_eq!(on - off, expected, "models={models}");
}
fn etd_width_delta_matches_text_column_growth() {
// When ETD is enabled the usage text column grows by exactly this delta,
// once per active model (see total_widget_width_for / model_usage_width),
// so the layout calc matches the wider draw rect and nothing re-clips.
let delta = ETD_TEXT_WIDTH - TEXT_WIDTH;
assert!(delta > 0);
assert_eq!(TEXT_WIDTH + delta, ETD_TEXT_WIDTH);
}
}