Previously, if you wanted to have a button that contains icons on both
edges, you'd need to use a `ButtonLike` component, which takes any
children. Meanwhile, the `Button` would only take one icon, where you
could control its position through the `IconPosition` enum. This has
always felt unnecessarily limiting. So, this PR removes this limitation
by adding two new methods to the button: `start_icon` and `end_icon`.
In the meantime, I have also been bothered by the unnecessary
indirection in the `IconButton` due to the existence of the `ButtonIcon`
component. So I figured I could also completely eliminate that by adding
some of its methods directly to the `IconButton` and in the Button, just
using a regular `Icon` component.
---
## Before
```rust
Button::new("id", "Label")
.icon(IconName::Plus)
.icon_position(IconPosition::Start)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
```
## After
```rust
Button::new("id", "Label")
.start_icon(Icon::new(IconName::Check))
.end_icon(Icon::new(IconName::ChevronDown).size(IconSize::XSmall))
```
This should have no visual impact to the UI.
Release Notes:
- N/A
215 lines
7.5 KiB
Rust
215 lines
7.5 KiB
Rust
use acp_thread::AgentSessionModes;
|
|
use agent_client_protocol as acp;
|
|
use agent_servers::AgentServer;
|
|
use agent_settings::AgentSettings;
|
|
use fs::Fs;
|
|
use gpui::{Context, Entity, WeakEntity, Window, prelude::*};
|
|
use settings::Settings as _;
|
|
use std::{rc::Rc, sync::Arc};
|
|
use ui::{
|
|
Button, ContextMenu, ContextMenuEntry, DocumentationSide, KeyBinding, PopoverMenu,
|
|
PopoverMenuHandle, Tooltip, prelude::*,
|
|
};
|
|
|
|
use crate::{CycleModeSelector, ToggleProfileSelector, ui::HoldForDefault};
|
|
|
|
pub struct ModeSelector {
|
|
connection: Rc<dyn AgentSessionModes>,
|
|
agent_server: Rc<dyn AgentServer>,
|
|
menu_handle: PopoverMenuHandle<ContextMenu>,
|
|
fs: Arc<dyn Fs>,
|
|
setting_mode: bool,
|
|
}
|
|
|
|
impl ModeSelector {
|
|
pub fn new(
|
|
session_modes: Rc<dyn AgentSessionModes>,
|
|
agent_server: Rc<dyn AgentServer>,
|
|
fs: Arc<dyn Fs>,
|
|
) -> Self {
|
|
Self {
|
|
connection: session_modes,
|
|
agent_server,
|
|
menu_handle: PopoverMenuHandle::default(),
|
|
fs,
|
|
setting_mode: false,
|
|
}
|
|
}
|
|
|
|
pub fn menu_handle(&self) -> PopoverMenuHandle<ContextMenu> {
|
|
self.menu_handle.clone()
|
|
}
|
|
|
|
pub fn cycle_mode(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
|
let all_modes = self.connection.all_modes();
|
|
let current_mode = self.connection.current_mode();
|
|
|
|
let current_index = all_modes
|
|
.iter()
|
|
.position(|mode| mode.id.0 == current_mode.0)
|
|
.unwrap_or(0);
|
|
|
|
let next_index = (current_index + 1) % all_modes.len();
|
|
self.set_mode(all_modes[next_index].id.clone(), cx);
|
|
}
|
|
|
|
pub fn mode(&self) -> acp::SessionModeId {
|
|
self.connection.current_mode()
|
|
}
|
|
|
|
pub fn set_mode(&mut self, mode: acp::SessionModeId, cx: &mut Context<Self>) {
|
|
let task = self.connection.set_mode(mode, cx);
|
|
self.setting_mode = true;
|
|
cx.notify();
|
|
|
|
cx.spawn(async move |this: WeakEntity<ModeSelector>, cx| {
|
|
if let Err(err) = task.await {
|
|
log::error!("Failed to set session mode: {:?}", err);
|
|
}
|
|
this.update(cx, |this, cx| {
|
|
this.setting_mode = false;
|
|
cx.notify();
|
|
})
|
|
.ok();
|
|
})
|
|
.detach();
|
|
}
|
|
|
|
fn build_context_menu(
|
|
&self,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> Entity<ContextMenu> {
|
|
let weak_self = cx.weak_entity();
|
|
|
|
ContextMenu::build(window, cx, move |mut menu, _window, cx| {
|
|
let all_modes = self.connection.all_modes();
|
|
let current_mode = self.connection.current_mode();
|
|
let default_mode = self.agent_server.default_mode(cx);
|
|
|
|
let settings = AgentSettings::get_global(cx);
|
|
let side = match settings.dock {
|
|
settings::DockPosition::Left => DocumentationSide::Right,
|
|
settings::DockPosition::Bottom | settings::DockPosition::Right => {
|
|
DocumentationSide::Left
|
|
}
|
|
};
|
|
|
|
for mode in all_modes {
|
|
let is_selected = &mode.id == ¤t_mode;
|
|
let is_default = Some(&mode.id) == default_mode.as_ref();
|
|
let entry = ContextMenuEntry::new(mode.name.clone())
|
|
.toggleable(IconPosition::End, is_selected);
|
|
|
|
let entry = if let Some(description) = &mode.description {
|
|
entry.documentation_aside(side, {
|
|
let description = description.clone();
|
|
|
|
move |_| {
|
|
v_flex()
|
|
.gap_1()
|
|
.child(Label::new(description.clone()))
|
|
.child(HoldForDefault::new(is_default))
|
|
.into_any_element()
|
|
}
|
|
})
|
|
} else {
|
|
entry
|
|
};
|
|
|
|
menu.push_item(entry.handler({
|
|
let mode_id = mode.id.clone();
|
|
let weak_self = weak_self.clone();
|
|
move |window, cx| {
|
|
weak_self
|
|
.update(cx, |this, cx| {
|
|
if window.modifiers().secondary() {
|
|
this.agent_server.set_default_mode(
|
|
if is_default {
|
|
None
|
|
} else {
|
|
Some(mode_id.clone())
|
|
},
|
|
this.fs.clone(),
|
|
cx,
|
|
);
|
|
}
|
|
|
|
this.set_mode(mode_id.clone(), cx);
|
|
})
|
|
.ok();
|
|
}
|
|
}));
|
|
}
|
|
|
|
menu.key_context("ModeSelector")
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Render for ModeSelector {
|
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let current_mode_id = self.connection.current_mode();
|
|
let current_mode_name = self
|
|
.connection
|
|
.all_modes()
|
|
.iter()
|
|
.find(|mode| mode.id == current_mode_id)
|
|
.map(|mode| mode.name.clone())
|
|
.unwrap_or_else(|| "Unknown".into());
|
|
|
|
let this = cx.weak_entity();
|
|
|
|
let icon = if self.menu_handle.is_deployed() {
|
|
IconName::ChevronUp
|
|
} else {
|
|
IconName::ChevronDown
|
|
};
|
|
|
|
let trigger_button = Button::new("mode-selector-trigger", current_mode_name)
|
|
.label_size(LabelSize::Small)
|
|
.color(Color::Muted)
|
|
.end_icon(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted))
|
|
.disabled(self.setting_mode);
|
|
|
|
PopoverMenu::new("mode-selector")
|
|
.trigger_with_tooltip(
|
|
trigger_button,
|
|
Tooltip::element({
|
|
move |_window, cx| {
|
|
v_flex()
|
|
.gap_1()
|
|
.child(
|
|
h_flex()
|
|
.gap_2()
|
|
.justify_between()
|
|
.child(Label::new("Change Mode"))
|
|
.child(KeyBinding::for_action(&ToggleProfileSelector, cx)),
|
|
)
|
|
.child(
|
|
h_flex()
|
|
.pt_1()
|
|
.gap_2()
|
|
.border_t_1()
|
|
.border_color(cx.theme().colors().border_variant)
|
|
.justify_between()
|
|
.child(Label::new("Cycle Through Modes"))
|
|
.child(KeyBinding::for_action(&CycleModeSelector, cx)),
|
|
)
|
|
.into_any()
|
|
}
|
|
}),
|
|
)
|
|
.anchor(gpui::Corner::BottomRight)
|
|
.with_handle(self.menu_handle.clone())
|
|
.offset(gpui::Point {
|
|
x: px(0.0),
|
|
y: px(-2.0),
|
|
})
|
|
.menu(move |window, cx| {
|
|
this.update(cx, |this, cx| this.build_context_menu(window, cx))
|
|
.ok()
|
|
})
|
|
}
|
|
}
|