gpui_cn/composer/
effort.rs1use gpui_kit::{
2 App, ElementId, Entity, IntoElement, ParentElement as _, RenderOnce, StyleRefinement, Styled,
3 Window,
4 assets::IconName,
5 base::{Align, StyledExt as _},
6 div, px, rems,
7};
8
9use crate::{
10 ActiveTheme as _, Button, ButtonSize, DropdownMenu, Icon, MenuEntry, MenuItem,
11 ModelPickerState,
12 collapse::{self, Need},
13};
14
15#[derive(IntoElement)]
40#[non_exhaustive]
41pub struct EffortMenu {
42 id: ElementId,
43 state: Entity<ModelPickerState>,
44 icon_only: bool,
45 style: StyleRefinement,
46}
47
48impl EffortMenu {
49 pub fn new(id: impl Into<ElementId>, state: &Entity<ModelPickerState>) -> Self {
51 Self {
52 id: id.into(),
53 state: state.clone(),
54 icon_only: false,
55 style: StyleRefinement::default(),
56 }
57 }
58
59 pub fn icon_only(mut self, icon_only: bool) -> Self {
62 self.icon_only = icon_only;
63 self
64 }
65
66 pub(crate) fn need(&self, window: &Window, cx: &App) -> Option<Need> {
70 let level = self.state.read(cx).effort()?;
71 let theme = cx.theme();
72 let padding = theme.metrics.control_padding_sm;
73 let rem = window.rem_size();
74 let gap = rems(0.375).to_pixels(rem);
75 let label = collapse::text_width(
76 &self.state.read(cx).effort_levels()[usize::from(level)],
77 window,
78 )
79 .min(collapse::min_label_width(window, collapse::MIN_LABEL_CHARS));
80 Some(Need::flexible(
81 padding * 2. + rems(1.).to_pixels(rem) + gap + label + gap + rems(0.75).to_pixels(rem),
82 Some(theme.metrics.control_sm),
83 ))
84 }
85}
86
87impl Styled for EffortMenu {
88 fn style(&mut self) -> &mut StyleRefinement {
89 &mut self.style
90 }
91}
92
93impl RenderOnce for EffortMenu {
94 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
95 let need = self.need(window, cx);
96 let (level, menu) = {
97 let state = self.state.read(cx);
98 (state.effort(), state.effort_menu_state().clone())
99 };
100 let Some(level) = level else {
101 return gpui_kit::div().into_any_element();
102 };
103 let muted = cx.theme().muted_foreground();
104 let word = self.state.read(cx).effort_levels()[usize::from(level)].clone();
105 let min = match (self.icon_only, need.and_then(|need| need.icon_only)) {
106 (true, Some(icon_only)) => icon_only,
107 _ => need.map_or(px(0.), |need| need.min),
108 };
109 let button = Button::new(ElementId::NamedChild(
110 self.id.clone().into(),
111 "trigger".into(),
112 ))
113 .ghost()
114 .size(ButtonSize::Sm)
115 .accessibility_label("Effort")
116 .min_w(min)
117 .text_color(muted)
118 .icon(Icon::from(IconName::Gauge));
119 let trigger = if self.icon_only {
120 button.tooltip(format!("Effort: {word}"))
121 } else {
122 let keep = collapse::text_width(&word, window)
123 .min(collapse::min_label_width(window, collapse::MIN_LABEL_CHARS));
124 button
125 .child(
126 div()
127 .min_w(keep)
128 .flex_shrink(1.)
129 .overflow_hidden()
130 .whitespace_nowrap()
131 .text_ellipsis()
132 .child(word),
133 )
134 .flex_shrink(1.)
135 .trailing_icon(Icon::from(IconName::ChevronDown).size_3())
136 };
137 let state = self.state.clone();
138 DropdownMenu::new(self.id, &menu)
139 .align(Align::End)
140 .refine_style(&self.style)
141 .trigger(trigger)
142 .items(move |_, cx| {
143 let current = state.read(cx).effort();
144 state
145 .read(cx)
146 .effort_levels()
147 .iter()
148 .enumerate()
149 .map(|(index, label)| {
150 MenuEntry::from(
151 MenuItem::new(index.to_string(), label.clone())
152 .checked(current == Some(index as u8)),
153 )
154 })
155 .collect()
156 })
157 .into_any_element()
158 }
159}