// Copyright 2026 Ling71671
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
/// Component style descriptor for vertical fader channel controls.
pub struct FaderStyle {
width : Double
height : Double
track_w : Double
cap_w : Double
cap_h : Double
cap_radius : Double
} derive(Eq, Debug)
///|
pub fn FaderStyle::default() -> FaderStyle {
{
width: 44.0,
height: 150.0,
track_w: 6.0,
cap_w: 32.0,
cap_h: 20.0,
cap_radius: 3.0,
}
}
///|
/// Standalone vertical audio channel fader parameter controller supporting fluent configuration.
pub struct Fader {
id_salt : String
label : String
value : Double
min_val : Double
max_val : Double
step : Double
default_val : Double
unit : String
show_ticks : Bool
size : @math.Vec2?
} derive(Eq, Debug)
///|
/// Create a new vertical fader widget with standard defaults.
pub fn Fader::new(id_salt : String, label : String, value : Double) -> Fader {
{
id_salt,
label,
value,
min_val: 0.0,
max_val: 1.0,
step: 0.01,
default_val: 0.0,
unit: "",
show_ticks: true,
size: None,
}
}
///|
/// Set minimum parameter value.
pub fn Fader::min(self : Fader, min_val : Double) -> Fader {
{ ..self, min_val, }
}
///|
/// Set maximum parameter value.
pub fn Fader::max(self : Fader, max_val : Double) -> Fader {
{ ..self, max_val, }
}
///|
/// Set step increment.
pub fn Fader::step(self : Fader, step : Double) -> Fader {
{ ..self, step, }
}
///|
/// Set default reset value (triggered on Alt+Click).
pub fn Fader::default_val(self : Fader, default_val : Double) -> Fader {
{ ..self, default_val, }
}
///|
/// Set display unit suffix (e.g. "dB", "Hz", "%").
pub fn Fader::unit(self : Fader, unit : String) -> Fader {
{ ..self, unit, }
}
///|
/// Toggle calibrated scale tick marks.
pub fn Fader::show_ticks(self : Fader, show_ticks : Bool) -> Fader {
{ ..self, show_ticks, }
}
///|
/// Override layout dimensions.
pub fn Fader::size(self : Fader, size : @math.Vec2) -> Fader {
{ ..self, size: Some(size), }
}
///|
/// Reference-bound fader widget for uniform `ui.add(...)` dispatch.
pub struct FaderRef {
fader : Fader
target : Ref[Double]
}
///|
/// Create a vertical fader bound directly to a `Ref[Double]`.
pub fn Fader::from_ref(
id_salt : String,
label : String,
target : Ref[Double],
) -> FaderRef {
{ fader: Fader::new(id_salt, label, target.val), target, }
}
///|
pub fn FaderRef::min(self : FaderRef, min_val : Double) -> FaderRef {
{ ..self, fader: self.fader.min(min_val), }
}
///|
pub fn FaderRef::max(self : FaderRef, max_val : Double) -> FaderRef {
{ ..self, fader: self.fader.max(max_val), }
}
///|
pub fn FaderRef::step(self : FaderRef, step : Double) -> FaderRef {
{ ..self, fader: self.fader.step(step), }
}
///|
pub fn FaderRef::default_val(self : FaderRef, default_val : Double) -> FaderRef {
{ ..self, fader: self.fader.default_val(default_val), }
}
///|
pub fn FaderRef::unit(self : FaderRef, unit : String) -> FaderRef {
{ ..self, fader: self.fader.unit(unit), }
}
///|
pub fn FaderRef::show_ticks(self : FaderRef, show_ticks : Bool) -> FaderRef {
{ ..self, fader: self.fader.show_ticks(show_ticks), }
}
///|
pub fn FaderRef::size(self : FaderRef, size : @math.Vec2) -> FaderRef {
{ ..self, fader: self.fader.size(size), }
}
///|
pub impl @core.Widget for FaderRef with fn ui(self, ctx : UIContext) -> Response {
let (new_val, resp) = { ..self.fader, value: self.target.val, }.show(ctx)
self.target.val = new_val
resp
}
///|
/// Render the fader and return the updated value alongside the interaction response.
pub fn Fader::show(self : Fader, ctx : UIContext) -> (Double, Response) {
let scale = ctx.style.scale
let font_nm = ctx.style.font_normal * scale
let font_sm = ctx.style.font_small * scale
let f_style = FaderStyle::default()
let total_w = match self.size {
Some(s) => s.x
None => f_style.width * scale
}
let total_h = match self.size {
Some(s) => s.y
None => f_style.height * scale
}
let track_w = f_style.track_w * scale
let cap_w = f_style.cap_w * scale
let cap_h = f_style.cap_h * scale
let cap_radius = f_style.cap_radius * scale
let min_val = self.min_val
let max_val = self.max_val
let step = self.step
let default_val = self.default_val
let unit = self.unit
let show_ticks = self.show_ticks
let label = self.label
let value = self.value
let range = if max_val > min_val { max_val - min_val } else { 1.0 }
let raw_val = if value.is_nan() { default_val } else { value }
let clamped_initial = if raw_val < min_val {
min_val
} else if raw_val > max_val {
max_val
} else {
raw_val
}
ctx.push_id(self.id_salt)
let (id, rect, response) = ctx.allocate_space(
@math.Vec2::new(total_w, total_h),
)
ctx.pop_id()
ctx.register_focusable(id)
if response.pressed() || response.clicked() {
ctx.request_focus(id)
}
if response.hovered() || response.dragged() || response.pressed() {
ctx.set_cursor_icon("ns-resize")
}
let is_focused = ctx.has_focus(id) || response.has_focus()
let label_h = if label.length() > 0 { font_nm + 6.0 * scale } else { 0.0 }
let readout_h = font_sm + 6.0 * scale
let track_top_y = rect.y + label_h + cap_h * 0.5
let track_bottom_y = rect.y + total_h - readout_h - cap_h * 0.5
let track_travel_h = if track_bottom_y > track_top_y {
track_bottom_y - track_top_y
} else {
1.0
}
let center_x = rect.x + total_w * 0.5
let mut current_val = clamped_initial
// 1. Mouse Drag (Vertical motion: upward drag increases value, downward decreases)
if !ctx.input.modifiers.alt &&
(response.dragged() || (response.pressed() && ctx.input.mouse_down)) {
let dy = -ctx.input.mouse_delta.y
let sensitivity = if ctx.input.modifiers.shift {
0.1
} else if ctx.input.modifiers.ctrl {
10.0
} else {
1.0
}
let delta = dy / track_travel_h * range * sensitivity
current_val = current_val + delta
}
// 2. Mouse Scroll Wheel
if response.hovered() && ctx.input.scroll_delta.y.abs() > 0.0 {
let dir = if ctx.input.scroll_delta.y > 0.0 { 1.0 } else { -1.0 }
let multiplier = if ctx.input.modifiers.shift { 0.1 } else { 1.0 }
current_val = current_val + dir * step * multiplier
}
// 3. Keyboard Navigation when focused
if is_focused {
let key_step = if ctx.input.modifiers.shift {
step * 0.1
} else if ctx.input.modifiers.ctrl {
step * 10.0
} else {
step
}
if ctx.input.key_pressed(Key::ArrowUp) ||
ctx.input.key_pressed(Key::ArrowRight) {
current_val = current_val + key_step
ctx.input.consume_key(Key::ArrowUp)
ctx.input.consume_key(Key::ArrowRight)
}
if ctx.input.key_pressed(Key::ArrowDown) ||
ctx.input.key_pressed(Key::ArrowLeft) {
current_val = current_val - key_step
ctx.input.consume_key(Key::ArrowDown)
ctx.input.consume_key(Key::ArrowLeft)
}
if ctx.input.key_pressed(Key::Home) {
current_val = max_val
ctx.input.consume_key(Key::Home)
}
if ctx.input.key_pressed(Key::End) {
current_val = min_val
ctx.input.consume_key(Key::End)
}
let page_step = key_step * 10.0
if ctx.input.key_pressed(Key::PageUp) {
current_val = current_val + page_step
ctx.input.consume_key(Key::PageUp)
}
if ctx.input.key_pressed(Key::PageDown) {
current_val = current_val - page_step
ctx.input.consume_key(Key::PageDown)
}
}
// 4. Alt + Click or double click reset to default
if response.clicked() && ctx.input.modifiers.alt {
current_val = default_val
}
// Final value clamping
if current_val.is_nan() {
current_val = default_val
}
if current_val < min_val {
current_val = min_val
} else if current_val > max_val {
current_val = max_val
}
// --- Rendering ---
let fraction = (current_val - min_val) / range
let clamped_frac = if fraction.is_nan() || fraction < 0.0 {
0.0
} else if fraction > 1.0 {
1.0
} else {
fraction
}
let cap_center_y = track_bottom_y - clamped_frac * track_travel_h
// A. Top Label
if label.length() > 0 {
let label_sz = ctx.measure_text(label, font_nm)
let label_x = center_x - label_sz.x * 0.5
let label_y = rect.y
ctx
.painter()
.add_text(
@math.Vec2::new(label_x, label_y),
label,
font_nm,
@color.Color::text_secondary(),
)
}
// B. Calibrated Tick Marks (Left & Right)
if show_ticks {
let tick_count = 11
let left_x = center_x - track_w * 0.5 - 4.0 * scale
let right_x = center_x + track_w * 0.5 + 4.0 * scale
for i = 0; i < tick_count; i = i + 1 {
let t = i.to_double() / (tick_count - 1).to_double()
let tick_y = track_bottom_y - t * track_travel_h
let is_major = i == 0 || i == 5 || i == 10
let tick_len = if is_major { 5.0 * scale } else { 3.0 * scale }
let tick_col = if is_major {
@color.Color::border_strong()
} else {
@color.Color::border_muted()
}
ctx
.painter()
.add_line(
@math.Vec2::new(left_x - tick_len, tick_y),
@math.Vec2::new(left_x, tick_y),
tick_col,
1.0,
)
ctx
.painter()
.add_line(
@math.Vec2::new(right_x, tick_y),
@math.Vec2::new(right_x + tick_len, tick_y),
tick_col,
1.0,
)
}
}
// C. Recessed Track Groove
let track_x = center_x - track_w * 0.5
let track_h_total = track_travel_h + cap_h
let track_y = track_top_y - cap_h * 0.5
let track_radius = 3.0 * scale
// Track shadow/recess
ctx
.painter()
.add_rect(
@math.Rect::new(track_x, track_y, track_w, track_h_total),
@color.Color::bg_surface(),
track_radius,
)
ctx
.painter()
.add_rect_stroke(
@math.Rect::new(track_x, track_y, track_w, track_h_total),
@color.Color::border_muted(),
1.0,
track_radius,
)
// Track active fill from bottom up to cap
let fill_h = track_bottom_y - cap_center_y
if fill_h > 0.0 {
let fill_y = cap_center_y
ctx
.painter()
.add_rect(
@math.Rect::new(
track_x + 1.0 * scale,
fill_y,
track_w - 2.0 * scale,
fill_h,
),
@color.Color::accent_primary().with_alpha(160),
2.0 * scale,
)
}
// D. Fader Cap Shadow
let cap_x = center_x - cap_w * 0.5
let cap_y = cap_center_y - cap_h * 0.5
let cap_rect = @math.Rect::new(cap_x, cap_y, cap_w, cap_h)
let is_active = response.dragged() ||
(response.pressed() && ctx.input.mouse_down)
ctx
.painter()
.add_rect(
cap_rect.translate(@math.Vec2::new(0.0, 2.0 * scale)),
@color.Color::shadow(),
cap_radius,
)
// E. Porcelain Tactile Fader Cap
let cap_bg = if is_active {
@color.Color::bg_active()
} else if response.hovered() {
@color.Color::bg_hover()
} else {
@color.Color::bg_window()
}
ctx.painter().add_rect(cap_rect, cap_bg, cap_radius)
// Cap border
let cap_border = if is_focused {
@color.Color::border_focus()
} else if response.hovered() || is_active {
@color.Color::border_strong()
} else {
@color.Color::border_default()
}
ctx.painter().add_rect_stroke(cap_rect, cap_border, 1.0, cap_radius)
// Center High-Contrast Index Line on Cap
let index_y = cap_center_y
let index_inset = 4.0 * scale
ctx
.painter()
.add_line(
@math.Vec2::new(cap_x + index_inset, index_y),
@math.Vec2::new(cap_x + cap_w - index_inset, index_y),
if is_active {
@color.Color::accent_primary()
} else {
@color.Color::border_strong()
},
1.5 * scale,
)
// Focus ring
if is_focused {
ctx
.painter()
.add_rect_stroke(
cap_rect.expand(2.0 * scale),
@color.Color::border_focus(),
1.5,
cap_radius + 2.0 * scale,
)
}
// F. Bottom Monospace Readout
let rounded_int = (current_val * 10.0).to_int()
let whole = rounded_int / 10
let frac_part = (rounded_int % 10).abs()
let mut readout_str = whole.to_string() + "." + frac_part.to_string()
if unit.length() > 0 {
readout_str = readout_str + unit
}
let readout_sz = ctx.measure_text(readout_str, font_sm)
let readout_x = center_x - readout_sz.x * 0.5
let readout_y = rect.y + total_h - font_sm
ctx
.painter()
.add_text(
@math.Vec2::new(readout_x, readout_y),
readout_str,
font_sm,
if is_active || is_focused {
@color.Color::accent_primary()
} else {
@color.Color::text_muted()
},
font_family="monospace",
)
(current_val, response)
}
///|
/// Immediate-mode vertical audio channel fader parameter controller.
/// Features vertical recessed track groove, calibrated tick marks,
/// tactile fader cap with center index indicator line, vertical drag delta,
/// scroll wheel stepping, Shift (0.1x) / Ctrl (10x) modifiers,
/// keyboard ArrowUp/ArrowDown navigation, and Alt+Click reset.
pub fn fader(
ctx : UIContext,
id_salt : String,
label : String,
value : Double,
min_val? : Double = 0.0,
max_val? : Double = 1.0,
step? : Double = 0.01,
default_val? : Double = 0.0,
unit? : String = "",
show_ticks? : Bool = true,
size? : @math.Vec2,
) -> (Double, Response) {
let mut f = Fader::new(id_salt, label, value)
.min(min_val)
.max(max_val)
.step(step)
.default_val(default_val)
.unit(unit)
.show_ticks(show_ticks)
if size is Some(s) {
f = f.size(s)
}
f.show(ctx)
}
///|
/// Integer variant of the vertical parameter fader.
pub fn fader_int(
ctx : UIContext,
id_salt : String,
label : String,
value : Int,
min_val? : Int = 0,
max_val? : Int = 100,
step? : Int = 1,
default_val? : Int = 0,
unit? : String = "",
show_ticks? : Bool = true,
size? : @math.Vec2,
) -> (Int, Response) {
let (new_val, resp) = match size {
Some(s) =>
fader(
ctx,
id_salt,
label,
value.to_double(),
min_val=min_val.to_double(),
max_val=max_val.to_double(),
step=step.to_double(),
default_val=default_val.to_double(),
unit~,
show_ticks~,
size=s,
)
None =>
fader(
ctx,
id_salt,
label,
value.to_double(),
min_val=min_val.to_double(),
max_val=max_val.to_double(),
step=step.to_double(),
default_val=default_val.to_double(),
unit~,
show_ticks~,
)
}
let rounded = if new_val >= 0.0 {
(new_val + 0.5).to_int()
} else {
(new_val - 0.5).to_int()
}
(rounded, resp)
}