Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/app/boot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ pub(crate) fn init() -> (AppState, Task<Message>) {
let mut global_pre_request_script = String::new();
let mut global_test_script = String::new();
let mut sidebar_width = 200.0;
let mut ui_scale = 1.0;
#[cfg(feature = "self-update")]
let mut auto_check_updates = false;
let mut tls_options = crate::services::http::TlsOptions::default();
Expand All @@ -69,6 +70,9 @@ pub(crate) fn init() -> (AppState, Task<Message>) {
global_pre_request_script = sess.global_pre_request_script.clone();
global_test_script = sess.global_test_script.clone();
sidebar_width = sess.sidebar_width;
if sess.ui_scale >= 0.5 && sess.ui_scale <= 2.0 {
ui_scale = sess.ui_scale;
}
#[cfg(feature = "self-update")]
{
auto_check_updates = sess.auto_check_updates;
Expand Down Expand Up @@ -196,7 +200,7 @@ pub(crate) fn init() -> (AppState, Task<Message>) {
panel_split: 50,
sidebar_width,
horizontal_layout: false,
ui_scale: 1.0,
ui_scale,
#[cfg(feature = "self-update")]
update: crate::app::UpdateState::Idle,
#[cfg(feature = "self-update")]
Expand Down
1 change: 1 addition & 0 deletions src/app/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pub(crate) fn persist_session(state: &mut AppState) {
active_env_id: state.active_env().map(|e| e.id.clone()),
sidebar_panel: format!("{:?}", state.sidebar.panel),
sidebar_width: state.sidebar_width,
ui_scale: state.ui_scale,
theme_idx: state.theme_idx,
default_timeout_ms: state.default_timeout_ms,
global_pre_request_script: state.global_pre_request_editor.text(),
Expand Down
9 changes: 7 additions & 2 deletions src/app/update/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,14 @@ fn dispatch(state: &mut AppState, message: Message) -> Task<Message> {

fn handle_layout(state: &mut AppState, msg: LayoutMsg) -> Task<Message> {
match msg {
LayoutMsg::ZoomIn => state.ui_scale = (state.ui_scale + 0.1).min(2.0),
LayoutMsg::ZoomOut => state.ui_scale = (state.ui_scale - 0.1).max(0.5),
LayoutMsg::ZoomIn => {
state.ui_scale = (((state.ui_scale + 0.1) * 10.0).round() / 10.0).min(2.0);
}
LayoutMsg::ZoomOut => {
state.ui_scale = (((state.ui_scale - 0.1) * 10.0).round() / 10.0).max(0.5);
}
LayoutMsg::ZoomReset => state.ui_scale = 1.0,
LayoutMsg::SetZoom(scale) => state.ui_scale = scale.clamp(0.5, 2.0),
LayoutMsg::PanelSplitChanged(split) => {
if split >= 1 && split <= 99 {
state.panel_split = split;
Expand Down
1 change: 1 addition & 0 deletions src/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,7 @@ pub enum LayoutMsg {
ZoomIn,
ZoomOut,
ZoomReset,
SetZoom(f64),
PanelSplitChanged(u16),
/// Drag on the sidebar/main-area splitter (issue #53): new sidebar width.
SidebarWidthChanged(f32),
Expand Down
28 changes: 28 additions & 0 deletions src/state/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ pub struct AppSession {
/// Sidebar content width in px (issue #53).
#[serde(default = "default_sidebar_width")]
pub sidebar_width: f32,
/// UI scale factor (scale_factor).
#[serde(default = "default_ui_scale")]
pub ui_scale: f64,
#[serde(default)]
pub theme_idx: usize,
#[serde(default = "default_timeout_ms")]
Expand Down Expand Up @@ -45,3 +48,28 @@ fn default_timeout_ms() -> u64 {
fn default_sidebar_width() -> f32 {
200.0
}

fn default_ui_scale() -> f64 {
1.0
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn session_defaults_ui_scale_when_missing() {
let json = r#"{"tabs":[],"active_tab":0,"sidebar_panel":"collections"}"#;
let session: AppSession = serde_json::from_str(json).unwrap();
assert_eq!(session.ui_scale, 1.0);
}

#[test]
fn session_roundtrips_ui_scale() {
let mut session = AppSession::default();
session.ui_scale = 1.25;
let json = serde_json::to_string(&session).unwrap();
let loaded: AppSession = serde_json::from_str(&json).unwrap();
assert_eq!(loaded.ui_scale, 1.25);
}
}
164 changes: 160 additions & 4 deletions src/ui/sidebar/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const LOGO_SVG: &[u8] = include_bytes!("../../../public/rustman-logo.svg");
#[cfg(feature = "self-update")]
use crate::{app::UpdateState, message::UpdateMsg};
use crate::{
message::{AppMsg, Message, SettingsMsg},
message::{AppMsg, LayoutMsg, Message, SettingsMsg},
ui::theme::{Palette, TEXT_SM, THEMES, ThemeSpec},
ui::tree_label::{tree_label, tree_label_styled},
};
Expand All @@ -21,7 +21,11 @@ pub fn view(state: &crate::app::AppState) -> Element<'_, Message> {
&state.git_user_email,
));
let appearance = card(build_appearance(state.theme_idx));
let layout = card(build_layout(state.horizontal_layout, state.sidebar_width));
let layout = card(build_layout(
state.horizontal_layout,
state.ui_scale,
state.sidebar_width,
));
let request_defaults = card(build_request_defaults(&state.default_timeout_text));
let tls = card(build_tls(state.tls_options));
let global_scripts = card(build_global_scripts(
Expand Down Expand Up @@ -376,7 +380,11 @@ fn swatch(color: Color) -> Element<'static, Message> {
.into()
}

fn build_layout(horizontal: bool, content_width: f32) -> Element<'static, Message> {
fn build_layout(
horizontal: bool,
ui_scale: f64,
content_width: f32,
) -> Element<'static, Message> {
let options = [("Top / Bottom", false), ("Left / Right", true)];

let mut buttons = Vec::new();
Expand Down Expand Up @@ -426,7 +434,7 @@ fn build_layout(horizontal: bool, content_width: f32) -> Element<'static, Messag
// Side-by-side needs ~95px per button ("Left / Right" ≈ 72px text +
// padding); below that the labels wrap inside the buttons and the row
// grows ragged. Stack vertically instead.
let mut col = column![section_label("LAYOUT")].spacing(6);
let mut col = column![section_label("LAYOUT"), field_label("Panels")].spacing(6);
if content_width >= 230.0 {
let mut row_el = row![].spacing(6);
for b in buttons {
Expand All @@ -438,6 +446,152 @@ fn build_layout(horizontal: bool, content_width: f32) -> Element<'static, Messag
col = col.push(b);
}
}

col = col.push(Space::new().height(4));
col = col.push(field_label("UI Scale"));

let pct = (ui_scale * 100.0).round() as i64;
let pct_label = format!("{pct}%");
let is_default = (ui_scale - 1.0).abs() < 0.01;

let btn_out = button(
text("−")
.size(TEXT_SM)
.color(Palette::text())
.align_x(iced::alignment::Horizontal::Center),
)
.on_press(Message::Layout(LayoutMsg::ZoomOut))
.style(|_t, s| {
use iced::widget::button::Status;
iced::widget::button::Style {
background: Some(Background::Color(if matches!(s, Status::Hovered) {
Palette::hover()
} else {
Palette::surface_high()
})),
border: Border {
color: Palette::border_subtle(),
width: 1.0,
radius: 6.0.into(),
},
..Default::default()
}
})
.padding([4, 12]);

let btn_reset = button(
text(pct_label)
.size(TEXT_SM)
.color(if is_default {
Palette::text()
} else {
Palette::accent()
})
.align_x(iced::alignment::Horizontal::Center),
)
.on_press(Message::Layout(LayoutMsg::ZoomReset))
.style(move |_t, s| {
use iced::widget::button::Status;
iced::widget::button::Style {
background: Some(Background::Color(if !is_default {
Palette::accent_soft()
} else if matches!(s, Status::Hovered) {
Palette::hover()
} else {
Palette::surface_high()
})),
border: Border {
color: if !is_default {
Palette::accent()
} else {
Palette::border_subtle()
},
width: 1.0,
radius: 6.0.into(),
},
..Default::default()
}
})
.padding([4, 8])
.width(Length::Fill);

let btn_in = button(
text("+")
.size(TEXT_SM)
.color(Palette::text())
.align_x(iced::alignment::Horizontal::Center),
)
.on_press(Message::Layout(LayoutMsg::ZoomIn))
.style(|_t, s| {
use iced::widget::button::Status;
iced::widget::button::Style {
background: Some(Background::Color(if matches!(s, Status::Hovered) {
Palette::hover()
} else {
Palette::surface_high()
})),
border: Border {
color: Palette::border_subtle(),
width: 1.0,
radius: 6.0.into(),
},
..Default::default()
}
})
.padding([4, 12]);

let zoom_row = row![btn_out, btn_reset, btn_in]
.spacing(6)
.align_y(iced::Alignment::Center);

col = col.push(zoom_row);

let presets = [0.8, 1.0, 1.25, 1.5];
let mut preset_row = row![].spacing(4);
for preset in presets {
let is_selected = (ui_scale - preset).abs() < 0.01;
let p_label = format!("{:.0}%", preset * 100.0);
let btn = button(
text(p_label)
.size(10)
.font(crate::ui::theme::MONO)
.color(if is_selected {
Palette::accent()
} else {
Palette::text_muted()
})
.align_x(iced::alignment::Horizontal::Center),
)
.on_press(Message::Layout(LayoutMsg::SetZoom(preset)))
.style(move |_t, s| {
use iced::widget::button::Status;
iced::widget::button::Style {
background: Some(Background::Color(if is_selected {
Palette::accent_soft()
} else if matches!(s, Status::Hovered) {
Palette::hover()
} else {
Palette::surface_high()
})),
border: Border {
color: if is_selected {
Palette::accent()
} else {
Palette::border_subtle()
},
width: 1.0,
radius: 4.0.into(),
},
..Default::default()
}
})
.padding([3, 4])
.width(Length::Fill);

preset_row = preset_row.push(btn);
}
col = col.push(preset_row);

col.into()
}

Expand Down Expand Up @@ -529,6 +683,8 @@ fn build_shortcuts(content_width: f32) -> Element<'static, Message> {
(format!("{cmd}E"), "Export cURL"),
(format!("{cmd}Enter"), "Send"),
(format!("{alt}1-9"), "Switch tab"),
(format!("{cmd}+/-"), "Zoom in/out"),
(format!("{cmd}0"), "Reset zoom"),
("Esc".to_owned(), "Close dialog"),
("↑/↓".to_owned(), "Navigate"),
];
Expand Down