diff --git a/src/localization/english.rs b/src/localization/english.rs index a8f7f88..5485d72 100644 --- a/src/localization/english.rs +++ b/src/localization/english.rs @@ -50,4 +50,7 @@ pub(super) const STRINGS: Strings = Strings { peak_err_tz_format: "UTC offset must be a number of hours (e.g. 10 for UTC+10, -5 for UTC-5).", peak_err_tz_range: "UTC offset must be between -12 and +14 hours.", peak_err_start_equals_end: "Start and end times must be different.", + peak_status_ok: "OK", + peak_status_warn: "WARN", + peak_status_peak: "PEAK", }; diff --git a/src/localization/french.rs b/src/localization/french.rs index 6d772af..985cbeb 100644 --- a/src/localization/french.rs +++ b/src/localization/french.rs @@ -49,4 +49,7 @@ pub(super) const STRINGS: Strings = Strings { peak_err_tz_format: "Le décalage UTC doit être un nombre d'heures (ex. 10 pour UTC+10, -5 pour UTC-5).", peak_err_tz_range: "Le décalage UTC doit être entre -12 et +14 heures.", peak_err_start_equals_end: "Les heures de début et de fin doivent être différentes.", + peak_status_ok: "OK", + peak_status_warn: "ALERTE", + peak_status_peak: "PIC", }; diff --git a/src/localization/german.rs b/src/localization/german.rs index d7fc029..ab33a6f 100644 --- a/src/localization/german.rs +++ b/src/localization/german.rs @@ -49,4 +49,7 @@ pub(super) const STRINGS: Strings = Strings { peak_err_tz_format: "UTC-Abweichung muss eine Anzahl von Stunden sein (z.B. 10 für UTC+10, -5 für UTC-5).", peak_err_tz_range: "UTC-Abweichung muss zwischen -12 und +14 Stunden liegen.", peak_err_start_equals_end: "Start- und Endzeit müssen unterschiedlich sein.", + peak_status_ok: "OK", + peak_status_warn: "WARN", + peak_status_peak: "PEAK", }; diff --git a/src/localization/japanese.rs b/src/localization/japanese.rs index deb7b3b..10a1b73 100644 --- a/src/localization/japanese.rs +++ b/src/localization/japanese.rs @@ -50,4 +50,7 @@ pub(super) const STRINGS: Strings = Strings { peak_err_tz_format: "UTCオフセットは時間数で入力してください(例: UTC+10は10、UTC-5は-5)。", peak_err_tz_range: "UTCオフセットは-12から+14時間の範囲で入力してください。", peak_err_start_equals_end: "開始時刻と終了時刻は異なる必要があります。", + peak_status_ok: "OK", + peak_status_warn: "注意", + peak_status_peak: "ピーク", }; diff --git a/src/localization/korean.rs b/src/localization/korean.rs index 5d1e78e..8b288c3 100644 --- a/src/localization/korean.rs +++ b/src/localization/korean.rs @@ -50,4 +50,7 @@ pub(super) const STRINGS: Strings = Strings { peak_err_tz_format: "UTC 오프셋은 시간 단위의 숫자여야 합니다 (예: UTC+10은 10, UTC-5는 -5).", peak_err_tz_range: "UTC 오프셋은 -12에서 +14 시간 사이여야 합니다.", peak_err_start_equals_end: "시작 시간과 종료 시간은 달라야 합니다.", + peak_status_ok: "OK", + peak_status_warn: "주의", + peak_status_peak: "피크", }; diff --git a/src/localization/mod.rs b/src/localization/mod.rs index 147258a..acbf1d5 100644 --- a/src/localization/mod.rs +++ b/src/localization/mod.rs @@ -141,6 +141,9 @@ pub struct Strings { pub peak_err_tz_format: &'static str, pub peak_err_tz_range: &'static str, pub peak_err_start_equals_end: &'static str, + pub peak_status_ok: &'static str, + pub peak_status_warn: &'static str, + pub peak_status_peak: &'static str, } pub fn resolve_language(language_override: Option) -> LanguageId { diff --git a/src/localization/spanish.rs b/src/localization/spanish.rs index 134a6e3..19b7bad 100644 --- a/src/localization/spanish.rs +++ b/src/localization/spanish.rs @@ -49,4 +49,7 @@ pub(super) const STRINGS: Strings = Strings { peak_err_tz_format: "El desfase UTC debe ser un número de horas (ej. 10 para UTC+10, -5 para UTC-5).", peak_err_tz_range: "El desfase UTC debe estar entre -12 y +14 horas.", peak_err_start_equals_end: "Las horas de inicio y fin deben ser diferentes.", + peak_status_ok: "OK", + peak_status_warn: "AVISO", + peak_status_peak: "PICO", }; diff --git a/src/peak_dialog.rs b/src/peak_dialog.rs index 46f72cf..e353880 100644 --- a/src/peak_dialog.rs +++ b/src/peak_dialog.rs @@ -9,6 +9,7 @@ use std::sync::Mutex; use windows::core::PCWSTR; use windows::Win32::Foundation::{HINSTANCE, HWND, LPARAM, WPARAM}; use windows::Win32::System::LibraryLoader::GetModuleHandleW; +use windows::Win32::System::SystemInformation::{GetLocalTime, GetSystemTime}; use windows::Win32::UI::WindowsAndMessaging::*; use crate::localization::Strings; @@ -216,6 +217,34 @@ fn get_text(hdlg: HWND, id: i32) -> String { } } +// ── System timezone helpers ───────────────────────────────────────────────────── + +/// Returns a string like "UTC+10", "UTC-5:30", or "UTC" for the current local offset. +fn local_tz_offset_str() -> String { + let (utc, local) = unsafe { (GetSystemTime(), GetLocalTime()) }; + let mut offset_min = (local.wHour as i32 - utc.wHour as i32) * 60 + + (local.wMinute as i32 - utc.wMinute as i32); + // Clamp across day boundaries (offsets are always in [-12h, +14h]) + if offset_min > 14 * 60 { + offset_min -= 24 * 60; + } else if offset_min < -12 * 60 { + offset_min += 24 * 60; + } + if offset_min == 0 { + "UTC".to_string() + } else { + let sign = if offset_min > 0 { "+" } else { "-" }; + let abs = offset_min.unsigned_abs(); + let h = abs / 60; + let m = abs % 60; + if m == 0 { + format!("UTC{}{}", sign, h) + } else { + format!("UTC{}{}:{:02}", sign, h, m) + } + } +} + // ── In-memory DLGTEMPLATE builder ───────────────────────────────────────────── struct Buf(Vec); @@ -352,7 +381,8 @@ fn build_template(strings: &Strings) -> Vec { strings.peak_tz_label ); item!(edt, 93, 44, 55, 12, IDC_TZ as u16, ATOM_EDIT, ""); - // Hint text + // Hint text (with resolved system timezone appended) + let hint_text = format!("{} [{}]", strings.peak_tz_hint, local_tz_offset_str()); item!( lbl, 7, @@ -361,7 +391,7 @@ fn build_template(strings: &Strings) -> Vec { 8, 0xFFFF_u16, ATOM_STATIC, - strings.peak_tz_hint + &hint_text ); // Buttons item!( diff --git a/src/window.rs b/src/window.rs index b54cc0e..4f5ab29 100644 --- a/src/window.rs +++ b/src/window.rs @@ -133,10 +133,10 @@ const IDM_PEAK_HOURS: u16 = 50; const DIVIDER_HIT_ZONE: i32 = 13; // LEFT_DIVIDER_W + DIVIDER_RIGHT_MARGIN -/// Diameter of the peak-hours status circle (px at 96 DPI). -/// Equals 2 × SEGMENT_H + row gap = 2 × 13 + 10 = 36, spanning both meter rows. +/// Width of the peak-status indicator column (px at 96 DPI). +/// Contains a colored rectangle on row 1 and a short text label on row 2. const PEAK_ICON_W: i32 = 36; -/// Gap between the right edge of the circle and the first meter label. +/// Gap between the right edge of the indicator and the first meter label. const PEAK_ICON_GAP: i32 = 6; const WM_DPICHANGED_MSG: u32 = 0x02E0; @@ -1362,7 +1362,7 @@ fn paint_content( let row1_y = row2_y - sc(10) - sc(SEGMENT_H); if peak_status != PeakStatus::NoPeakHours { - draw_peak_circle(hdc, icon_x, height, peak_status, is_dark); + draw_peak_indicator(hdc, icon_x, height, peak_status, is_dark, text_color, strings); } let _ = SetBkMode(hdc, TRANSPARENT); @@ -2507,15 +2507,23 @@ fn draw_rounded_rect(hdc: HDC, rect: &RECT, color: &Color, radius: i32) { } } -/// Draw the peak-hours status circle. +/// Draw the peak-hours status indicator as two rows matching the meter layout. /// -/// The circle is a filled GDI ellipse whose diameter spans the full height of -/// both meter rows (= PEAK_ICON_W = 36 px at 96 DPI), centred vertically in -/// the widget. Colours adapt to the current theme: +/// Row 1: a colored rounded rectangle (same height as a bar segment). +/// Row 2: a short text label ("OK", "WARN", "PEAK" or localised equivalent). +/// Colours adapt to the current theme: /// - Outside (>60 min until peak): green /// - Warning (<60 min until peak): amber /// - Inside (within peak hours): red -fn draw_peak_circle(hdc: HDC, x: i32, height: i32, status: PeakStatus, is_dark: bool) { +fn draw_peak_indicator( + hdc: HDC, + x: i32, + height: i32, + status: PeakStatus, + is_dark: bool, + text_color: &Color, + strings: Strings, +) { let color = match (status, is_dark) { (PeakStatus::Outside, true) => Color::from_hex("#22C55E"), (PeakStatus::Outside, false) => Color::from_hex("#16A34A"), @@ -2526,23 +2534,66 @@ fn draw_peak_circle(hdc: HDC, x: i32, height: i32, status: PeakStatus, is_dark: (PeakStatus::NoPeakHours, _) => return, }; - // Align circle top/bottom with the meter rows. - let row2_y = height - sc(5) - sc(SEGMENT_H); - let row1_y = row2_y - sc(10) - sc(SEGMENT_H); - let top = row1_y; - let bottom = row2_y + sc(SEGMENT_H); - let right = x + sc(PEAK_ICON_W); + let label = match status { + PeakStatus::Outside => strings.peak_status_ok, + PeakStatus::Warning => strings.peak_status_warn, + PeakStatus::Inside => strings.peak_status_peak, + PeakStatus::NoPeakHours => return, + }; + let seg_h = sc(SEGMENT_H); + let row2_y = height - sc(5) - seg_h; + let row1_y = row2_y - sc(10) - seg_h; + let w = sc(PEAK_ICON_W); + + // Row 1: colored rectangle + let rect = RECT { + left: x, + top: row1_y, + right: x + w, + bottom: row1_y + seg_h, + }; + draw_rounded_rect(hdc, &rect, &color, sc(CORNER_RADIUS)); + + // Row 2: text label unsafe { - let brush = CreateSolidBrush(COLORREF(color.to_colorref())); - let old_brush = SelectObject(hdc, brush); - let null_pen = GetStockObject(NULL_PEN); - let old_pen = SelectObject(hdc, null_pen); + let _ = SetBkMode(hdc, TRANSPARENT); + let _ = SetTextColor(hdc, COLORREF(text_color.to_colorref())); - let _ = Ellipse(hdc, x, top, right, bottom); + let font_name = native_interop::wide_str("Segoe UI"); + let font = CreateFontW( + sc(-10), + 0, + 0, + 0, + FW_MEDIUM.0 as i32, + 0, + 0, + 0, + DEFAULT_CHARSET.0 as u32, + OUT_TT_PRECIS.0 as u32, + CLIP_DEFAULT_PRECIS.0 as u32, + CLEARTYPE_QUALITY.0 as u32, + (DEFAULT_PITCH.0 | FF_DONTCARE.0) as u32, + PCWSTR::from_raw(font_name.as_ptr()), + ); + let old_font = SelectObject(hdc, font); - SelectObject(hdc, old_pen); - SelectObject(hdc, old_brush); - let _ = DeleteObject(brush); + let mut label_wide: Vec = label.encode_utf16().collect(); + let mut label_rect = RECT { + left: x, + top: row2_y, + right: x + w, + bottom: row2_y + seg_h, + }; + let _ = DrawTextW( + hdc, + &mut label_wide, + &mut label_rect, + DT_CENTER | DT_VCENTER | DT_SINGLELINE, + ); + + SelectObject(hdc, old_font); + let _ = DeleteObject(font); } }