Better peak indicator shape

This commit is contained in:
A333 2026-04-16 21:44:27 +03:00
parent 5b7c6a6f68
commit 2f3c747e4a
9 changed files with 127 additions and 25 deletions

View File

@ -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",
};

View File

@ -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",
};

View File

@ -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",
};

View File

@ -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: "ピーク",
};

View File

@ -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: "피크",
};

View File

@ -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>) -> LanguageId {

View File

@ -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",
};

View File

@ -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<u8>);
@ -352,7 +381,8 @@ fn build_template(strings: &Strings) -> Vec<u8> {
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<u8> {
8,
0xFFFF_u16,
ATOM_STATIC,
strings.peak_tz_hint
&hint_text
);
// Buttons
item!(

View File

@ -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<u16> = 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);
}
}