refactor: replace standard sliders with PlainSlider for better UI consistency

This commit is contained in:
2026-07-19 01:53:51 +03:00
parent 9388e6f096
commit e90cb2d31d
8 changed files with 228 additions and 170 deletions
@@ -2,8 +2,9 @@
use crate::app::Message;
use crate::theme::ThemeColors;
use iced::widget::{button, column, horizontal_rule, row, slider, text};
use iced::{Element, Length};
use crate::widgets::plain_slider::plain_slider;
use iced::widget::{column, horizontal_rule, row, text};
use iced::Element;
/// Build the Brightness/Contrast adjustment dialog.
pub fn brightness_contrast_view(
@@ -11,30 +12,35 @@ pub fn brightness_contrast_view(
contrast: f32,
colors: ThemeColors,
) -> Element<'static, Message> {
let brightness_slider = slider(-100.0..=100.0, brightness, |v| Message::AdjBrightness(v))
.step(1.0)
.width(Length::Fill);
let brightness_slider = plain_slider(
"Brightness",
brightness,
-100.0..=100.0,
1.0,
"",
0,
|v| Message::AdjBrightness(v),
);
let contrast_slider = slider(-100.0..=100.0, contrast, |v| Message::AdjContrast(v))
.step(1.0)
.width(Length::Fill);
let contrast_slider = plain_slider(
"Contrast",
contrast,
-100.0..=100.0,
1.0,
"",
0,
|v| Message::AdjContrast(v),
);
let apply_btn = button(text("Apply").size(11))
.on_press(Message::AdjApply)
.padding([6, 12]);
let cancel_btn = button(text("Cancel").size(11))
.on_press(Message::DialogClose)
.padding([6, 12]);
let apply_btn = super::primary_btn("Apply", Message::AdjApply, colors);
let cancel_btn = super::secondary_btn("Cancel", Message::DialogClose, colors);
let dialog = column![
text("Brightness / Contrast")
.size(14)
.font(iced::Font::MONOSPACE),
horizontal_rule(1),
text(format!("Brightness: {:.0}", brightness)).size(11),
brightness_slider,
text(format!("Contrast: {:.0}", contrast)).size(11),
contrast_slider,
horizontal_rule(1),
row![apply_btn, cancel_btn].spacing(12),
@@ -53,36 +59,28 @@ pub fn hsl_view(
lightness: f32,
colors: ThemeColors,
) -> Element<'static, Message> {
let hue_slider = slider(-180.0..=180.0, hue, |v| Message::AdjHue(v))
.step(1.0)
.width(Length::Fill);
let hue_slider = plain_slider("Hue", hue, -180.0..=180.0, 1.0, "°", 0, |v| {
Message::AdjHue(v)
});
let sat_slider = slider(-100.0..=100.0, saturation, |v| Message::AdjSaturation(v))
.step(1.0)
.width(Length::Fill);
let sat_slider = plain_slider("Saturation", saturation, -100.0..=100.0, 1.0, "", 0, |v| {
Message::AdjSaturation(v)
});
let light_slider = slider(-100.0..=100.0, lightness, |v| Message::AdjLightness(v))
.step(1.0)
.width(Length::Fill);
let light_slider = plain_slider("Lightness", lightness, -100.0..=100.0, 1.0, "", 0, |v| {
Message::AdjLightness(v)
});
let apply_btn = button(text("Apply").size(11))
.on_press(Message::AdjApply)
.padding([6, 12]);
let cancel_btn = button(text("Cancel").size(11))
.on_press(Message::DialogClose)
.padding([6, 12]);
let apply_btn = super::primary_btn("Apply", Message::AdjApply, colors);
let cancel_btn = super::secondary_btn("Cancel", Message::DialogClose, colors);
let dialog = column![
text("Hue / Saturation / Lightness")
.size(14)
.font(iced::Font::MONOSPACE),
horizontal_rule(1),
text(format!("Hue: {:.0}°", hue)).size(11),
hue_slider,
text(format!("Saturation: {:.0}", saturation)).size(11),
sat_slider,
text(format!("Lightness: {:.0}", lightness)).size(11),
light_slider,
horizontal_rule(1),
row![apply_btn, cancel_btn].spacing(12),
@@ -2,24 +2,17 @@
use crate::app::Message;
use crate::theme::ThemeColors;
use iced::widget::{button, column, horizontal_rule, row, slider, text};
use iced::{Element, Length};
use crate::widgets::plain_slider::plain_slider;
use iced::widget::{column, horizontal_rule, row, text};
use iced::Element;
/// Build a confirmation dialog with save/discard/cancel.
pub fn close_confirm_view(doc_name: &str, colors: ThemeColors) -> Element<'static, Message> {
let msg = format!("\"{}\" has unsaved changes.", doc_name);
let save_btn = button(text("Save").size(12))
.on_press(Message::DialogCloseSave)
.padding([8, 16]);
let discard_btn = button(text("Discard").size(12))
.on_press(Message::DialogCloseDiscard)
.padding([8, 16]);
let cancel_btn = button(text("Cancel").size(12))
.on_press(Message::DialogClose)
.padding([8, 16]);
let save_btn = super::primary_btn("Save", Message::DialogCloseSave, colors);
let discard_btn = super::secondary_btn("Discard", Message::DialogCloseDiscard, colors);
let cancel_btn = super::secondary_btn("Cancel", Message::DialogClose, colors);
let dialog = column![
text("Unsaved Changes").size(16).font(iced::Font::MONOSPACE),
@@ -44,22 +37,16 @@ pub fn selection_op_view(
colors: ThemeColors,
) -> Element<'static, Message> {
let op_owned = op_name.to_string();
let value_slider = slider(min..=max, value, |v| Message::SelectionOpValue(v))
.step(1.0)
.width(Length::Fill);
let value_slider = plain_slider("Value", value, min..=max, 1.0, "", 0, |v| {
Message::SelectionOpValue(v)
});
let apply_btn = button(text("Apply").size(11))
.on_press(Message::SelectionOpApply)
.padding([6, 12]);
let cancel_btn = button(text("Cancel").size(11))
.on_press(Message::DialogClose)
.padding([6, 12]);
let apply_btn = super::primary_btn("Apply", Message::SelectionOpApply, colors);
let cancel_btn = super::secondary_btn("Cancel", Message::DialogClose, colors);
let dialog = column![
text(op_owned).size(14).font(iced::Font::MONOSPACE),
horizontal_rule(1),
text(format!("Value: {:.0}", value)).size(11),
value_slider,
horizontal_rule(1),
row![apply_btn, cancel_btn].spacing(12),
@@ -2,8 +2,8 @@
use crate::app::Message;
use crate::theme::ThemeColors;
use iced::widget::container;
use iced::{Element, Length};
use iced::widget::{button, container, text};
use iced::{Border, Element, Length};
pub mod about;
pub mod adjustments;
@@ -55,3 +55,85 @@ pub fn modal<'a>(
.height(Length::Fill)
.into()
}
/// Blend a color with white by a factor (0.0 = no blend, 1.0 = full white).
fn lighten(c: iced::Color, factor: f32) -> iced::Color {
iced::Color::new(
c.r + (1.0 - c.r) * factor,
c.g + (1.0 - c.g) * factor,
c.b + (1.0 - c.b) * factor,
c.a,
)
}
/// Blend a color with black by a factor (0.0 = no blend, 1.0 = full black).
fn darken(c: iced::Color, factor: f32) -> iced::Color {
iced::Color::new(
c.r * (1.0 - factor),
c.g * (1.0 - factor),
c.b * (1.0 - factor),
c.a,
)
}
/// Modern theme-compatible primary button (accent fill).
pub fn primary_btn(
label: &str,
msg: Message,
colors: ThemeColors,
) -> Element<'static, Message> {
let l = label.to_string();
let c = colors;
button(text(l).size(12).style(move |_theme: &iced::Theme| iced::widget::text::Style {
color: Some(iced::Color::WHITE),
}))
.on_press(msg)
.padding([8, 16])
.style(move |_theme, status| {
let bg = match status {
iced::widget::button::Status::Hovered => lighten(c.accent, 0.15),
iced::widget::button::Status::Pressed => darken(c.accent, 0.15),
_ => c.accent,
};
iced::widget::button::Style {
background: Some(iced::Background::Color(bg)),
text_color: iced::Color::WHITE,
border: Border::default().rounded(6),
..Default::default()
}
})
.into()
}
/// Modern theme-compatible secondary button (bordered ghost).
pub fn secondary_btn(
label: &str,
msg: Message,
colors: ThemeColors,
) -> Element<'static, Message> {
let l = label.to_string();
let c = colors;
button(text(l).size(12).style(move |_theme: &iced::Theme| iced::widget::text::Style {
color: Some(c.text_primary),
}))
.on_press(msg)
.padding([8, 16])
.style(move |_theme, status| {
let bg = match status {
iced::widget::button::Status::Hovered => {
let mut h = c.bg_hover;
h.a *= 0.5;
h
}
iced::widget::button::Status::Pressed => c.bg_hover,
_ => iced::Color::TRANSPARENT,
};
iced::widget::button::Style {
background: Some(iced::Background::Color(bg)),
text_color: c.text_primary,
border: Border::default().rounded(6).color(c.border_high).width(1),
..Default::default()
}
})
.into()
}
@@ -2,8 +2,9 @@
use crate::app::Message;
use crate::theme::ThemeColors;
use crate::widgets::plain_slider::plain_slider;
use iced::widget::{
button, checkbox, column, horizontal_rule, row, scrollable, slider, text, text_input,
button, checkbox, column, horizontal_rule, row, scrollable, text, text_input,
};
use iced::{Element, Length};
@@ -70,16 +71,25 @@ pub fn view(
.on_input(|s| Message::DialogNewImageName(s))
.width(Length::Fill);
let width_text = text(format!("Width: {}", width)).size(11);
let height_text = text(format!("Height: {}", height)).size(11);
let width_slider = plain_slider(
"Width",
width as f32,
1.0..=4096.0,
1.0,
"px",
0,
|v| Message::DialogNewImageWidth(v as u32),
);
let width_slider = slider(1_u32..=4096, width, |v| Message::DialogNewImageWidth(v))
.step(1_u32)
.width(Length::Fill);
let height_slider = slider(1_u32..=4096, height, |v| Message::DialogNewImageHeight(v))
.step(1_u32)
.width(Length::Fill);
let height_slider = plain_slider(
"Height",
height as f32,
1.0..=4096.0,
1.0,
"px",
0,
|v| Message::DialogNewImageHeight(v as u32),
);
let transparent_check = checkbox("Transparent background", transparent)
.on_toggle(|v| Message::DialogNewImageTransparent(v));
@@ -96,22 +106,15 @@ pub fn view(
presets_col = presets_col.push(preset_btn);
}
let create_btn = button(text("Create").size(12))
.on_press(Message::DialogNewImageCreate)
.padding([8, 16]);
let cancel_btn = button(text("Cancel").size(12))
.on_press(Message::DialogClose)
.padding([8, 16]);
let create_btn = super::primary_btn("Create", Message::DialogNewImageCreate, colors);
let cancel_btn = super::secondary_btn("Cancel", Message::DialogClose, colors);
let dialog = column![
text("New Image").size(16).font(iced::Font::MONOSPACE),
horizontal_rule(1),
text("Name:").size(11),
name_input,
width_text,
width_slider,
height_text,
height_slider,
transparent_check,
horizontal_rule(1),
@@ -10,8 +10,9 @@
use crate::app::Message;
use crate::theme::ThemeColors;
use crate::widgets::plain_slider::plain_slider;
use hcie_engine_api::FilterType;
use iced::widget::{column, container, row, slider, text};
use iced::widget::{column, container, row, text};
use iced::{Element, Length};
use serde_json::json;
@@ -35,11 +36,11 @@ fn param_str<'a>(params: &'a serde_json::Value, key: &str, default: &'a str) ->
params.get(key).and_then(|v| v.as_str()).unwrap_or(default)
}
/// Build a labeled float slider row.
/// Build a labeled float slider row with keyboard editing.
///
/// Returns a row with label, slider, and numeric display. The slider
/// Returns a column with label and keyboard-editable slider. The slider
/// emits `Message::FilterParamChanged(filter_id, json!({key: new_value}))`
/// when the user drags it.
/// when the user drags or types a value.
fn float_slider(
label: &str,
key: &str,
@@ -50,30 +51,28 @@ fn float_slider(
step: f64,
_colors: ThemeColors,
) -> Element<'static, Message> {
let val = value as f32;
let min_f = min as f32;
let max_f = max as f32;
let step_f = step as f32;
let key_owned = key.to_string();
let filter_owned = filter_id.to_string();
let label_owned = label.to_string();
let step_f = step as f32;
let lbl = text(label_owned).size(11).width(Length::Fixed(80.0));
let display = text(format!("{:.1}", value))
.size(11)
.width(Length::Fixed(40.0));
let s = slider(min_f..=max_f, val, move |v| {
let rounded = (v as f64 / step).round() * step;
Message::FilterParamChanged(filter_owned.clone(), json!({ key_owned.clone(): rounded }))
})
.step(step_f)
.width(Length::Fill);
row![lbl, s, display].spacing(4).into()
plain_slider(
label,
value as f32,
(min as f32)..=(max as f32),
step_f,
"",
1,
move |v| {
let rounded = (v as f64 / step).round() * step;
Message::FilterParamChanged(
filter_owned.clone(),
json!({ key_owned.clone(): rounded }),
)
},
)
}
/// Build a labeled integer slider row.
/// Build a labeled integer slider row with keyboard editing.
fn int_slider(
label: &str,
key: &str,
@@ -83,28 +82,23 @@ fn int_slider(
max: u64,
_colors: ThemeColors,
) -> Element<'static, Message> {
let val = value as f32;
let min_f = min as f32;
let max_f = max as f32;
let key_owned = key.to_string();
let filter_owned = filter_id.to_string();
let label_owned = label.to_string();
let lbl = text(label_owned).size(11).width(Length::Fixed(80.0));
let display = text(format!("{}", value))
.size(11)
.width(Length::Fixed(40.0));
let s = slider(min_f..=max_f, val, move |v| {
Message::FilterParamChanged(
filter_owned.clone(),
json!({ key_owned.clone(): v.round() as u64 }),
)
})
.step(1.0)
.width(Length::Fill);
row![lbl, s, display].spacing(4).into()
plain_slider(
label,
value as f32,
(min as f32)..=(max as f32),
1.0,
"",
0,
move |v| {
Message::FilterParamChanged(
filter_owned.clone(),
json!({ key_owned.clone(): v.round() as u64 }),
)
},
)
}
/// Build a section header for a group of parameters.
@@ -12,9 +12,10 @@ use crate::panels::styles;
use crate::panels::thumbnails;
use crate::panels::typography::BODY;
use crate::theme::ThemeColors;
use crate::widgets::plain_slider::plain_slider;
use hcie_engine_api::{BlendMode, LayerInfo, LayerType};
use iced::widget::{
button, column, container, horizontal_rule, pick_list, row, scrollable, slider, text, Space,
button, column, container, horizontal_rule, pick_list, row, scrollable, text, Space,
};
use iced::{Element, Length};
@@ -129,10 +130,10 @@ fn layer_thumbnail<'a>(
colors: ThemeColors,
) -> Element<'a, Message> {
if entry.is_group {
return container(text("\u{1F4C1}").size(13))
.width(28)
.height(20)
.center_y(Length::Fixed(20.0))
return container(text("\u{1F4C1}").size(16))
.width(50)
.height(38)
.center_y(Length::Fixed(38.0))
.into();
}
@@ -140,15 +141,15 @@ fn layer_thumbnail<'a>(
let thumb = engine
.get_layer_pixels(info.id)
.filter(|px| !px.is_empty() && info.width > 0 && info.height > 0)
.map(|px| thumbnails::generate_thumbnail(&px, info.width, info.height, 28));
.map(|px| thumbnails::generate_thumbnail(&px, info.width, info.height, 48));
if let Some(pixels) = thumb {
let handle = iced::widget::image::Handle::from_rgba(28, 20, pixels);
let img = iced::widget::Image::new(handle).width(28).height(20);
let handle = iced::widget::image::Handle::from_rgba(48, 36, pixels);
let img = iced::widget::Image::new(handle).width(48).height(36);
return container(img)
.width(30)
.height(22)
.center_y(Length::Fixed(22.0))
.width(50)
.height(38)
.center_y(Length::Fixed(38.0))
.padding(1)
.style(move |_theme| iced::widget::container::Style {
border: iced::Border::default().color(colors.border_high).width(1),
@@ -165,15 +166,15 @@ fn layer_thumbnail<'a>(
_ => ("\u{25A3}", colors.text_secondary),
};
container(text(icon_char).size(10).style(move |_theme: &iced::Theme| {
container(text(icon_char).size(12).style(move |_theme: &iced::Theme| {
iced::widget::text::Style {
color: Some(icon_color),
}
}))
.width(30)
.height(22)
.center_x(Length::Fixed(30.0))
.center_y(Length::Fixed(22.0))
.width(50)
.height(38)
.center_x(Length::Fixed(50.0))
.center_y(Length::Fixed(38.0))
.style(move |_theme| iced::widget::container::Style {
background: Some(iced::Background::Color(iced::Color::from_rgba(
1.0, 1.0, 1.0, 0.05,
@@ -222,18 +223,17 @@ pub fn view<'a>(
.width(Length::Fill)
.text_size(BODY);
let opacity_label = text(format!("{}%", (active_opacity * 100.0) as u32))
.size(BODY)
.width(Length::Fixed(32.0));
let opacity_slider = slider(0.0..=1.0, active_opacity, move |v| {
Message::LayerSetOpacity(active_layer_id, v)
})
.step(0.01)
.width(Length::Fill);
let opacity_slider = plain_slider(
"Opacity",
active_opacity,
0.0..=1.0,
0.01,
"%",
0,
move |v| Message::LayerSetOpacity(active_layer_id, v),
);
let opacity_row = row![text("Op").size(BODY), opacity_slider, opacity_label]
.spacing(3)
.align_y(iced::Alignment::Center);
let opacity_row = opacity_slider;
let lock_icon = if active_locked {
"\u{1F512}"
@@ -293,14 +293,14 @@ pub fn view<'a>(
Space::with_width(Length::Fixed(12.0)).into()
};
// Visibility toggle
let vis_char = if info.visible { "\u{25CF}" } else { "\u{25CB}" };
// Visibility toggle (eye icon)
let vis_char = if info.visible { "\u{1F441}" } else { "\u{1F441}" };
let vis_color = if info.visible {
colors.text_primary
} else {
colors.text_secondary
iced::Color { a: colors.text_secondary.a * 0.4, ..colors.text_secondary }
};
let vis_btn = button(text(vis_char).size(10).style(move |_theme: &iced::Theme| {
let vis_btn = button(text(vis_char).size(11).style(move |_theme: &iced::Theme| {
iced::widget::text::Style {
color: Some(vis_color),
}
@@ -15,9 +15,10 @@ use crate::app::{Message, ToolState};
use crate::panels::styles;
use crate::settings::AppSettings;
use crate::theme::ThemeColors;
use crate::widgets::plain_slider::plain_slider;
use hcie_engine_api::Tool;
use iced::widget::{
button, column, container, horizontal_rule, row, scrollable, slider, text, text_input,
button, column, container, horizontal_rule, row, scrollable, text, text_input,
};
use iced::{Element, Length};
@@ -462,19 +463,7 @@ fn prop_slider<F>(
where
F: Fn(f32) -> Message + 'static,
{
let l = label.to_string();
let label_text = text(l).size(10).width(Length::Fixed(70.0));
let val_text = text(format!("{:.1}", value))
.size(10)
.width(Length::Fixed(40.0));
let sl = slider(min..=max, value, msg_fn)
.step(0.1)
.width(Length::Fill);
row![label_text, sl, val_text]
.spacing(4)
.align_y(iced::Alignment::Center)
.into()
plain_slider(label, value, min..=max, 0.1, "", 1, msg_fn)
}
fn color_row(label: &str, color: [u8; 4], _c: ThemeColors) -> Element<'static, Message> {
+7 -2
View File
@@ -294,14 +294,19 @@ fn render_shape(layer: &mut Layer, shape: &VectorShape) {
let (rot_x3, rot_y3) = rotate_point(rx3, ry3, cx, cy, *angle);
let (rot_x4, rot_y4) = rotate_point(rx4, ry4, cx, cy, *angle);
draw_line(layer, rot_x1, rot_y1, rot_x2, rot_y2, px_color, *stroke, None);
let head_mid_x = (rot_x3 + rot_x4) / 2.0;
let head_mid_y = (rot_y3 + rot_y4) / 2.0;
draw_line(layer, rot_x1, rot_y1, head_mid_x, head_mid_y, px_color, *stroke, None);
draw_line(layer, rot_x2, rot_y2, rot_x3, rot_y3, px_color, *stroke, None);
draw_line(layer, rot_x2, rot_y2, rot_x4, rot_y4, px_color, *stroke, None);
draw_line(layer, rot_x3, rot_y3, rot_x4, rot_y4, px_color, *stroke, None);
if *fill {
let fill_alpha = (fill_color[3] as f32 * opacity).round() as u8;
let px_fill = [fill_color[0], fill_color[1], fill_color[2], fill_alpha];
draw_line(layer, rot_x2, rot_y2, rot_x3, rot_y3, px_fill, arrow_head_len, None);
let arrowhead_pts = vec![(rot_x2, rot_y2), (rot_x3, rot_y3), (rot_x4, rot_y4)];
fill_polygon(layer, &arrowhead_pts, px_fill);
}
}
Star { x1, y1, x2, y2, stroke, color, fill_color, fill, points, inner_radius, opacity, angle, .. } => {