liora_components/
operation.rs1use crate::Label;
2use gpui::{
3 AnyElement, App, Component, Hsla, IntoElement, Pixels, RenderOnce, SharedString, Window, div,
4 prelude::*, px,
5};
6use liora_core::Config;
7
8pub struct Operation {
9 label: AnyElement,
10 action: AnyElement,
11 description: Option<SharedString>,
12 status: Option<SharedString>,
13 status_color: Option<Hsla>,
14 gap: Pixels,
15 padded: bool,
16 disabled: bool,
17}
18
19impl Operation {
20 pub fn new(label: impl IntoElement, action: impl IntoElement) -> Self {
21 Self {
22 label: label.into_any_element(),
23 action: action.into_any_element(),
24 description: None,
25 status: None,
26 status_color: None,
27 gap: px(16.0),
28 padded: true,
29 disabled: false,
30 }
31 }
32
33 pub fn with_text(text: impl Into<gpui::SharedString>, action: impl IntoElement) -> Self {
34 Self::new(Label::new(text), action)
35 }
36 pub fn gap(mut self, gap: impl Into<Pixels>) -> Self {
37 self.gap = gap.into().max(px(0.0));
38 self
39 }
40 pub fn description(mut self, description: impl Into<SharedString>) -> Self {
41 self.description = Some(description.into());
42 self
43 }
44 pub fn status(mut self, status: impl Into<SharedString>) -> Self {
45 self.status = Some(status.into());
46 self
47 }
48 pub fn status_color(mut self, color: Hsla) -> Self {
49 self.status_color = Some(color);
50 self
51 }
52 pub fn success(self) -> Self {
53 self.status("正常").status_color(gpui::green())
54 }
55 pub fn warning(self) -> Self {
56 self.status("注意").status_color(gpui::yellow())
57 }
58 pub fn danger(self) -> Self {
59 self.status("异常").status_color(gpui::red())
60 }
61 pub fn disabled(mut self, disabled: bool) -> Self {
62 self.disabled = disabled;
63 self
64 }
65 pub fn no_padding(mut self) -> Self {
66 self.padded = false;
67 self
68 }
69}
70
71impl RenderOnce for Operation {
72 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
73 let theme = cx.global::<Config>().theme.clone();
74 let status_color = self.status_color.unwrap_or(theme.primary.base);
75 div()
76 .flex()
77 .items_center()
78 .justify_between()
79 .gap(self.gap)
80 .w_full()
81 .when(self.disabled, |s| s.opacity(0.52))
82 .when(self.padded, |s| {
83 s.p_3()
84 .rounded_md()
85 .border_1()
86 .border_color(theme.neutral.border)
87 .bg(theme.neutral.card)
88 })
89 .child(
90 div()
91 .min_w_0()
92 .flex()
93 .flex_col()
94 .gap_1()
95 .child(
96 div()
97 .flex()
98 .items_center()
99 .gap_2()
100 .child(self.label)
101 .when_some(self.status, |s, status| {
102 s.child(
103 div()
104 .rounded_full()
105 .px_2()
106 .py(px(1.0))
107 .text_xs()
108 .bg(status_color.opacity(0.12))
109 .text_color(status_color)
110 .child(status),
111 )
112 }),
113 )
114 .when_some(self.description, |s, description| {
115 s.child(
116 div()
117 .text_sm()
118 .text_color(theme.neutral.text_3)
119 .child(description),
120 )
121 }),
122 )
123 .child(div().flex_none().child(self.action))
124 }
125}
126
127impl IntoElement for Operation {
128 type Element = Component<Self>;
129 fn into_element(self) -> Self::Element {
130 Component::new(self)
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 #[test]
138 fn operation_tracks_layout_options() {
139 let op = Operation::with_text("Auto save", div())
140 .gap(px(20.0))
141 .description("Save changes automatically")
142 .status("Enabled")
143 .disabled(true)
144 .no_padding();
145 assert_eq!(op.gap, px(20.0));
146 assert!(!op.padded);
147 assert_eq!(
148 op.description.as_ref().map(|text| text.as_ref()),
149 Some("Save changes automatically")
150 );
151 assert_eq!(
152 op.status.as_ref().map(|text| text.as_ref()),
153 Some("Enabled")
154 );
155 assert!(op.disabled);
156 }
157}