// 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.
///|
/// Render a smooth, continuous loading spinner animation driven by context time,
/// featuring an asymmetric breathing sweep arc with trailing opacity taper.
pub fn spinner(
ctx : UIContext,
size? : Double = 20.0,
color? : @color.Color,
stroke_width? : Double = 2.5,
) -> Response {
let theme = ctx.theme
let eff_color = match color {
Some(c) => c
None => theme.accent_primary
}
let scale = ctx.style.scale
let eff_size = size * scale
let eff_stroke = stroke_width * scale
let (_id, rect, resp) = ctx.allocate_space(
@math.Vec2::new(eff_size, eff_size),
)
let center = rect.center()
let radius = (eff_size - eff_stroke) * 0.5
// 1. Subtle Background Track Ring
let p = ctx.painter()
p.add_circle_stroke(center, radius, theme.border_muted, eff_stroke)
// 2. Dynamic Arc Angles Driven by UIContext::time
let time = if ctx.time.is_nan() { 0.0 } else { ctx.time }
let pi2 = 6.283185307179586
let base_rot = time * 4.0 % pi2
let sweep_min = 0.6
let sweep_max = 4.8
let sweep_factor = (@math.sin(time * 2.8) + 1.0) * 0.5
let sweep_len = sweep_min + sweep_factor * (sweep_max - sweep_min)
// 3. Tapered Arc Line Segments
let num_steps = 18
for i = 0; i < num_steps; i = i + 1 {
let t1 = i.to_double() / num_steps.to_double()
let t2 = (i + 1).to_double() / num_steps.to_double()
let a1 = base_rot + t1 * sweep_len
let a2 = base_rot + t2 * sweep_len
let p1 = center.add(@math.Vec2::from_polar(radius, a1))
let p2 = center.add(@math.Vec2::from_polar(radius, a2))
let alpha = (80.0 + t2 * 175.0).to_int()
let seg_color = eff_color.with_alpha(alpha)
p.add_line(p1, p2, seg_color, eff_stroke)
}
resp
}
///|
/// Render a loading spinner inline with an adjacent text label.
pub fn spinner_with_label(
ctx : UIContext,
label : String,
size? : Double = 18.0,
color? : @color.Color,
stroke_width? : Double = 2.0,
) -> Response {
let theme = ctx.theme
let eff_color = match color {
Some(c) => c
None => theme.accent_primary
}
let scale = ctx.style.scale
let eff_size = size * scale
let eff_stroke = stroke_width * scale
let font_sz = ctx.style.font_normal * scale
let label_sz = ctx.measure_text(label, font_sz)
let gap = 8.0 * scale
let total_w = eff_size + gap + label_sz.x
let total_h = if eff_size > label_sz.y { eff_size } else { label_sz.y }
let (_id, rect, resp) = ctx.allocate_space(@math.Vec2::new(total_w, total_h))
let center = @math.Vec2::new(rect.x + eff_size * 0.5, rect.y + rect.h * 0.5)
let radius = (eff_size - eff_stroke) * 0.5
// Background ring
let p = ctx.painter()
p.add_circle_stroke(center, radius, theme.border_muted, eff_stroke)
// Arc
let time = if ctx.time.is_nan() { 0.0 } else { ctx.time }
let pi2 = 6.283185307179586
let base_rot = time * 4.0 % pi2
let sweep_factor = (@math.sin(time * 2.8) + 1.0) * 0.5
let sweep_len = 0.6 + sweep_factor * 4.2
let num_steps = 16
for i = 0; i < num_steps; i = i + 1 {
let t1 = i.to_double() / num_steps.to_double()
let t2 = (i + 1).to_double() / num_steps.to_double()
let a1 = base_rot + t1 * sweep_len
let a2 = base_rot + t2 * sweep_len
let p1 = center.add(@math.Vec2::from_polar(radius, a1))
let p2 = center.add(@math.Vec2::from_polar(radius, a2))
let alpha = (90.0 + t2 * 165.0).to_int()
let seg_color = eff_color.with_alpha(alpha)
p.add_line(p1, p2, seg_color, eff_stroke)
}
// Label text
let text_pos = @math.Vec2::new(
rect.x + eff_size + gap,
rect.y + (rect.h - font_sz) * 0.5,
)
p.add_text(text_pos, label, font_sz, theme.text_secondary)
resp
}