// 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.
///|
/// Semantic palette category for badges and tags.
pub(all) enum BadgeKind {
Default
Success
Warning
Danger
Info
} derive(Eq, Debug)
///|
pub fn BadgeKind::default() -> BadgeKind {
Default
}
///|
pub fn BadgeKind::success() -> BadgeKind {
Success
}
///|
pub fn BadgeKind::warning() -> BadgeKind {
Warning
}
///|
pub fn BadgeKind::danger() -> BadgeKind {
Danger
}
///|
pub fn BadgeKind::info() -> BadgeKind {
Info
}
///|
fn badge_colors(
kind : BadgeKind,
theme : @core.Theme,
) -> (@color.Color, @color.Color, @color.Color) {
match kind {
Default => (theme.bg_subtle, theme.border_default, theme.text_secondary)
Success =>
(
theme.success.with_alpha(35),
theme.success.with_alpha(80),
theme.success,
)
Warning =>
(
theme.warning.with_alpha(35),
theme.warning.with_alpha(80),
theme.warning,
)
Danger =>
(theme.danger.with_alpha(35), theme.danger.with_alpha(80), theme.danger)
Info =>
(
theme.accent_soft,
theme.accent_primary.with_alpha(80),
theme.accent_primary,
)
}
}
///|
/// Standalone semantic badge widget supporting fluent configuration.
pub struct Badge {
text : String
kind : BadgeKind
dot : Bool
pill : Bool
} derive(Eq, Debug)
///|
/// Create a new badge widget with default category.
pub fn Badge::new(text : String) -> Badge {
{ text, kind: Default, dot: false, pill: false, }
}
///|
/// Set the semantic category of the badge.
pub fn Badge::kind(self : Badge, kind : BadgeKind) -> Badge {
{ ..self, kind, }
}
///|
/// Enable or disable breathing status dot.
pub fn Badge::dot(self : Badge, dot : Bool) -> Badge {
{ ..self, dot, }
}
///|
/// Enable or disable pill capsule rounded corners.
pub fn Badge::pill(self : Badge, pill : Bool) -> Badge {
{ ..self, pill, }
}
///|
/// Render the badge into the provided UIContext.
pub impl @core.Widget for Badge with fn ui(self, ctx : UIContext) -> Response {
badge(ctx, self.text, kind=self.kind, dot=self.dot, pill=self.pill)
}
///|
/// Render a compact semantic status badge capsule with optional breathing status dot
/// and pill-shaped fully rounded borders.
pub fn badge(
ctx : UIContext,
text : String,
kind? : BadgeKind = Default,
dot? : Bool = false,
pill? : Bool = false,
) -> Response {
let scale = ctx.style.scale
let font_sz = ctx.style.font_small * scale
let text_sz = ctx.measure_text(text, font_sz)
let dot_w = if dot { ctx.style.spacing_md * scale } else { 0.0 }
let pad_x = ctx.style.spacing_sm * scale
let h = (ctx.style.control_h_sm - ctx.style.radius_xs) * scale
let w = pad_x * 2.0 + dot_w + text_sz.x
let (_id, rect, resp) = ctx.allocate_space(@math.Vec2::new(w, h))
let radius = if pill { h * 0.5 } else { ctx.style.radius_sm * scale }
let (bg, border, fg) = badge_colors(kind, ctx.theme)
let p = ctx.painter()
p.add_rect(rect, bg, radius)
p.add_rect_stroke(rect, border, 1.0 * scale, radius)
let mut current_x = rect.x + pad_x
// Status dot
if dot {
let dot_r = 3.0 * scale
let dot_center = @math.Vec2::new(current_x + dot_r, rect.y + h * 0.5)
p.add_circle(dot_center, dot_r, fg)
current_x = current_x + dot_w
}
// Text
let text_y = rect.y + (h - font_sz) * 0.5
p.add_text(@math.Vec2::new(current_x, text_y), text, font_sz, fg)
resp
}
///|
/// Render an interactive tag chip with optional close button (returning true when clicked).
pub fn tag(
ctx : UIContext,
id_salt : String,
text : String,
kind? : BadgeKind = Default,
closable? : Bool = false,
) -> (Bool, Response) {
let scale = ctx.style.scale
let font_sz = ctx.style.font_small * scale
let text_sz = ctx.measure_text(text, font_sz)
let close_w = if closable { 16.0 * scale } else { 0.0 }
let pad_x = 8.0 * scale
let h = 24.0 * scale
let w = pad_x * 2.0 + text_sz.x + close_w
ctx.push_id(id_salt)
let (id, rect, resp) = ctx.allocate_space(@math.Vec2::new(w, h))
ctx.pop_id()
ctx.register_focusable(id)
let is_focused = resp.has_focus() || ctx.has_focus(id)
let (bg, border, fg) = badge_colors(kind, ctx.theme)
let tag_radius = ctx.style.radius_sm * scale
let p = ctx.painter()
p.add_rect(rect, bg, tag_radius)
p.add_rect_stroke(
rect,
if is_focused {
ctx.theme.border_focus
} else {
border
},
if is_focused {
1.5 * scale
} else {
1.0 * scale
},
tag_radius,
)
// Label text
let text_y = rect.y + (h - font_sz) * 0.5
p.add_text(@math.Vec2::new(rect.x + pad_x, text_y), text, font_sz, fg)
let mut closed = false
// Closable button
if closable {
let btn_sz = 14.0 * scale
let close_box = @math.Rect::new(
rect.x + w - pad_x - btn_sz,
rect.y + (h - btn_sz) * 0.5,
btn_sz,
btn_sz,
)
let is_close_hovered = ctx.is_hovered(close_box)
if is_close_hovered {
ctx.set_cursor_icon("pointer")
p.add_rect(close_box, ctx.theme.bg_hover, tag_radius)
if ctx.input.mouse_pressed {
closed = true
}
}
// Keyboard close while the tag holds focus
if is_focused &&
(ctx.input.key_pressed(Key::Space) || ctx.input.key_pressed(Key::Enter)) {
ctx.input.consume_key(Key::Space)
ctx.input.consume_key(Key::Enter)
closed = true
}
let x_center = close_box.center()
let r = 3.5 * scale
p.add_line(
@math.Vec2::new(x_center.x - r, x_center.y - r),
@math.Vec2::new(x_center.x + r, x_center.y + r),
fg,
1.2 * scale,
)
p.add_line(
@math.Vec2::new(x_center.x - r, x_center.y + r),
@math.Vec2::new(x_center.x + r, x_center.y - r),
fg,
1.2 * scale,
)
}
(closed, resp)
}