Detailed changes
@@ -3313,11 +3313,20 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
&mut self,
element_id: usize,
initial: T,
+ ) -> ElementStateHandle<T> {
+ self.element_state_dynamic(TypeTag::new::<Tag>(), element_id, initial)
+ }
+
+ pub fn element_state_dynamic<T: 'static>(
+ &mut self,
+ tag: TypeTag,
+ element_id: usize,
+ initial: T,
) -> ElementStateHandle<T> {
let id = ElementStateId {
view_id: self.view_id(),
element_id,
- tag: TypeId::of::<Tag>(),
+ tag,
};
self.element_states
.entry(id)
@@ -3331,11 +3340,20 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
) -> ElementStateHandle<T> {
self.element_state::<Tag, T>(element_id, T::default())
}
+
+ pub fn default_element_state_dynamic<T: 'static + Default>(
+ &mut self,
+ tag: TypeTag,
+ element_id: usize,
+ ) -> ElementStateHandle<T> {
+ self.element_state_dynamic::<T>(tag, element_id, T::default())
+ }
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct TypeTag {
tag: TypeId,
+ composed: Option<TypeId>,
#[cfg(debug_assertions)]
tag_type_name: &'static str,
}
@@ -3344,6 +3362,7 @@ impl TypeTag {
pub fn new<Tag: 'static>() -> Self {
Self {
tag: TypeId::of::<Tag>(),
+ composed: None,
#[cfg(debug_assertions)]
tag_type_name: std::any::type_name::<Tag>(),
}
@@ -3352,11 +3371,17 @@ impl TypeTag {
pub fn dynamic(tag: TypeId, #[cfg(debug_assertions)] type_name: &'static str) -> Self {
Self {
tag,
+ composed: None,
#[cfg(debug_assertions)]
tag_type_name: type_name,
}
}
+ pub fn compose(mut self, other: TypeTag) -> Self {
+ self.composed = Some(other.tag);
+ self
+ }
+
#[cfg(debug_assertions)]
pub(crate) fn type_name(&self) -> &'static str {
self.tag_type_name
@@ -4751,7 +4776,7 @@ impl Hash for AnyWeakViewHandle {
pub struct ElementStateId {
view_id: usize,
element_id: usize,
- tag: TypeId,
+ tag: TypeTag,
}
pub struct ElementStateHandle<T> {
@@ -1,10 +1,13 @@
use std::any::{Any, TypeId};
+use crate::TypeTag;
+
pub trait Action: 'static {
fn id(&self) -> TypeId;
fn namespace(&self) -> &'static str;
fn name(&self) -> &'static str;
fn as_any(&self) -> &dyn Any;
+ fn type_tag(&self) -> TypeTag;
fn boxed_clone(&self) -> Box<dyn Action>;
fn eq(&self, other: &dyn Action) -> bool;
@@ -107,6 +110,10 @@ macro_rules! __impl_action {
}
}
+ fn type_tag(&self) -> $crate::TypeTag {
+ $crate::TypeTag::new::<Self>()
+ }
+
$from_json_fn
}
};
@@ -34,8 +34,8 @@ use crate::{
rect::RectF,
vector::{vec2f, Vector2F},
},
- json, Action, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View, ViewContext,
- WeakViewHandle, WindowContext,
+ json, Action, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, TypeTag, View,
+ ViewContext, WeakViewHandle, WindowContext,
};
use anyhow::{anyhow, Result};
use collections::HashMap;
@@ -172,6 +172,20 @@ pub trait Element<V: View>: 'static {
FlexItem::new(self.into_any()).float()
}
+ fn with_dynamic_tooltip(
+ self,
+ tag: TypeTag,
+ id: usize,
+ text: impl Into<Cow<'static, str>>,
+ action: Option<Box<dyn Action>>,
+ style: TooltipStyle,
+ cx: &mut ViewContext<V>,
+ ) -> Tooltip<V>
+ where
+ Self: 'static + Sized,
+ {
+ Tooltip::new_dynamic(tag, id, text, action, style, self.into_any(), cx)
+ }
fn with_tooltip<Tag: 'static>(
self,
id: usize,
@@ -7,6 +7,34 @@ use crate::{
ViewContext,
};
+use super::Empty;
+
+pub trait GeneralComponent {
+ fn render<V: View>(self, v: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V>;
+}
+
+pub trait StyleableComponent {
+ type Style: Clone;
+ type Output: GeneralComponent;
+
+ fn with_style(self, style: Self::Style) -> Self::Output;
+}
+
+impl GeneralComponent for () {
+ fn render<V: View>(self, _: &mut V, _: &mut ViewContext<V>) -> AnyElement<V> {
+ Empty::new().into_any()
+ }
+}
+
+impl StyleableComponent for () {
+ type Style = ();
+ type Output = ();
+
+ fn with_style(self, _: Self::Style) -> Self::Output {
+ ()
+ }
+}
+
pub trait Component<V: View> {
fn render(self, v: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V>;
@@ -18,6 +46,12 @@ pub trait Component<V: View> {
}
}
+impl<V: View, C: GeneralComponent> Component<V> for C {
+ fn render(self, v: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V> {
+ self.render(v, cx)
+ }
+}
+
pub struct ComponentAdapter<V, E> {
component: Option<E>,
phantom: PhantomData<V>,
@@ -7,7 +7,7 @@ use crate::{
geometry::{rect::RectF, vector::Vector2F},
json::json,
Action, Axis, ElementStateHandle, LayoutContext, PaintContext, SceneBuilder, SizeConstraint,
- Task, View, ViewContext,
+ Task, TypeTag, View, ViewContext,
};
use schemars::JsonSchema;
use serde::Deserialize;
@@ -61,11 +61,23 @@ impl<V: View> Tooltip<V> {
child: AnyElement<V>,
cx: &mut ViewContext<V>,
) -> Self {
- struct ElementState<Tag>(Tag);
- struct MouseEventHandlerState<Tag>(Tag);
+ Self::new_dynamic(TypeTag::new::<Tag>(), id, text, action, style, child, cx)
+ }
+
+ pub fn new_dynamic(
+ mut tag: TypeTag,
+ id: usize,
+ text: impl Into<Cow<'static, str>>,
+ action: Option<Box<dyn Action>>,
+ style: TooltipStyle,
+ child: AnyElement<V>,
+ cx: &mut ViewContext<V>,
+ ) -> Self {
+ tag = tag.compose(TypeTag::new::<Self>());
+
let focused_view_id = cx.focused_view_id();
- let state_handle = cx.default_element_state::<ElementState<Tag>, Rc<TooltipState>>(id);
+ let state_handle = cx.default_element_state_dynamic::<Rc<TooltipState>>(tag, id);
let state = state_handle.read(cx).clone();
let text = text.into();
@@ -95,7 +107,7 @@ impl<V: View> Tooltip<V> {
} else {
None
};
- let child = MouseEventHandler::new::<MouseEventHandlerState<Tag>, _>(id, cx, |_, _| child)
+ let child = MouseEventHandler::new_dynamic(tag, id, cx, |_, _| child)
.on_hover(move |e, _, cx| {
let position = e.position;
if e.started {
@@ -19,6 +19,7 @@ use gpui::{
use project::search::SearchQuery;
use serde::Deserialize;
use std::{any::Any, sync::Arc};
+
use util::ResultExt;
use workspace::{
item::ItemHandle,
@@ -167,23 +168,17 @@ impl View for BufferSearchBar {
cx,
)
};
- let render_search_option =
- |options: bool, icon, option, cx: &mut ViewContext<BufferSearchBar>| {
- options.then(|| {
- let is_active = self.search_options.contains(option);
- crate::search_bar::render_option_button_icon::<Self>(
- is_active,
- icon,
- option.bits as usize,
- format!("Toggle {}", option.label()),
- option.to_toggle_action(),
- move |_, this, cx| {
- this.toggle_search_option(option, cx);
- },
- cx,
- )
- })
- };
+ let render_search_option = |options: bool, icon, option| {
+ options.then(|| {
+ let is_active = self.search_options.contains(option);
+ option.as_button(
+ is_active,
+ icon,
+ theme.tooltip.clone(),
+ theme.search.option_button_component.clone(),
+ )
+ })
+ };
let match_count = self
.active_searchable_item
.as_ref()
@@ -242,13 +237,11 @@ impl View for BufferSearchBar {
supported_options.case,
"icons/case_insensitive_12.svg",
SearchOptions::CASE_SENSITIVE,
- cx,
))
.with_children(render_search_option(
supported_options.word,
"icons/word_search_12.svg",
SearchOptions::WHOLE_WORD,
- cx,
))
.flex_float()
.contained(),
@@ -1,9 +1,14 @@
use bitflags::bitflags;
pub use buffer_search::BufferSearchBar;
-use gpui::{actions, Action, AppContext};
+use gpui::{
+ actions,
+ elements::{Component, StyleableComponent, TooltipStyle},
+ Action, AnyElement, AppContext, Element, View,
+};
pub use mode::SearchMode;
use project::search::SearchQuery;
pub use project_search::{ProjectSearchBar, ProjectSearchView};
+use theme::components::{action_button::ActionButton, ComponentExt, ToggleIconButtonStyle};
pub mod buffer_search;
mod history;
@@ -69,4 +74,23 @@ impl SearchOptions {
options.set(SearchOptions::CASE_SENSITIVE, query.case_sensitive());
options
}
+
+ pub fn as_button<V: View>(
+ &self,
+ active: bool,
+ icon: &str,
+ tooltip_style: TooltipStyle,
+ button_style: ToggleIconButtonStyle,
+ ) -> AnyElement<V> {
+ ActionButton::new_dynamic(
+ self.to_toggle_action(),
+ format!("Toggle {}", self.label()),
+ tooltip_style,
+ )
+ .with_contents(theme::components::svg::Svg::new(icon.to_owned()))
+ .toggleable(active)
+ .with_style(button_style)
+ .into_element()
+ .into_any()
+ }
}
@@ -1,7 +1,9 @@
+pub mod components;
mod theme_registry;
mod theme_settings;
pub mod ui;
+use components::ToggleIconButtonStyle;
use gpui::{
color::Color,
elements::{ContainerStyle, ImageStyle, LabelStyle, Shadow, SvgStyle, TooltipStyle},
@@ -13,7 +15,7 @@ use serde::{de::DeserializeOwned, Deserialize};
use serde_json::Value;
use settings::SettingsStore;
use std::{collections::HashMap, sync::Arc};
-use ui::{ButtonStyle, CheckboxStyle, IconStyle, ModalStyle};
+use ui::{CheckboxStyle, CopilotCTAButton, IconStyle, ModalStyle};
pub use theme_registry::*;
pub use theme_settings::*;
@@ -182,7 +184,7 @@ pub struct CopilotAuth {
pub prompting: CopilotAuthPrompting,
pub not_authorized: CopilotAuthNotAuthorized,
pub authorized: CopilotAuthAuthorized,
- pub cta_button: ButtonStyle,
+ pub cta_button: CopilotCTAButton,
pub header: IconStyle,
}
@@ -196,7 +198,7 @@ pub struct CopilotAuthPrompting {
#[derive(Deserialize, Default, Clone, JsonSchema)]
pub struct DeviceCode {
pub text: TextStyle,
- pub cta: ButtonStyle,
+ pub cta: CopilotCTAButton,
pub left: f32,
pub left_container: ContainerStyle,
pub right: f32,
@@ -420,6 +422,7 @@ pub struct Search {
pub invalid_include_exclude_editor: ContainerStyle,
pub include_exclude_inputs: ContainedText,
pub option_button: Toggleable<Interactive<IconButton>>,
+ pub option_button_component: ToggleIconButtonStyle,
pub action_button: Toggleable<Interactive<ContainedText>>,
pub match_background: Color,
pub match_index: ContainedText,
@@ -887,12 +890,32 @@ pub struct Interactive<T> {
pub disabled: Option<T>,
}
+impl Interactive<()> {
+ pub fn new_blank() -> Self {
+ Self {
+ default: (),
+ hovered: None,
+ clicked: None,
+ disabled: None,
+ }
+ }
+}
+
#[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema)]
pub struct Toggleable<T> {
active: T,
inactive: T,
}
+impl Toggleable<()> {
+ pub fn new_blank() -> Self {
+ Self {
+ active: (),
+ inactive: (),
+ }
+ }
+}
+
impl<T> Toggleable<T> {
pub fn new(active: T, inactive: T) -> Self {
Self { active, inactive }
@@ -145,12 +145,12 @@ pub fn keystroke_label<V: View>(
.with_style(label_style.container)
}
-pub type ButtonStyle = Interactive<ContainedText>;
+pub type CopilotCTAButton = Interactive<ContainedText>;
pub fn cta_button<Tag, L, V, F>(
label: L,
max_width: f32,
- style: &ButtonStyle,
+ style: &CopilotCTAButton,
cx: &mut ViewContext<V>,
f: F,
) -> MouseEventHandler<V>
@@ -320,7 +320,6 @@ impl Pane {
can_drop: Rc::new(|_, _| true),
can_split: true,
render_tab_bar_buttons: Rc::new(move |pane, cx| {
- let tooltip_style = theme::current(cx).tooltip.clone();
Flex::row()
// New menu
.with_child(Self::render_tab_bar_button(
@@ -96,6 +96,63 @@ export default function search(): any {
},
},
}),
+ option_button_component: toggleable({
+ base: interactive({
+ base: {
+ icon_size: 14,
+ color: foreground(theme.highest, "variant"),
+
+ button_width: 32,
+ background: background(theme.highest, "on"),
+ corner_radius: 2,
+ margin: { right: 2 },
+ border: {
+ width: 1., color: background(theme.highest, "on")
+ },
+ padding: {
+ left: 4,
+ right: 4,
+ top: 4,
+ bottom: 4,
+ },
+ },
+ state: {
+ hovered: {
+ ...text(theme.highest, "mono", "variant", "hovered"),
+ background: background(theme.highest, "on", "hovered"),
+ border: {
+ width: 1., color: background(theme.highest, "on", "hovered")
+ },
+ },
+ clicked: {
+ ...text(theme.highest, "mono", "variant", "pressed"),
+ background: background(theme.highest, "on", "pressed"),
+ border: {
+ width: 1., color: background(theme.highest, "on", "pressed")
+ },
+ },
+ },
+ }),
+ state: {
+ active: {
+ default: {
+ icon_size: 14,
+ button_width: 32,
+ color: foreground(theme.highest, "variant"),
+ background: background(theme.highest, "accent"),
+ border: border(theme.highest, "accent"),
+ },
+ hovered: {
+ background: background(theme.highest, "accent", "hovered"),
+ border: border(theme.highest, "accent", "hovered"),
+ },
+ clicked: {
+ background: background(theme.highest, "accent", "pressed"),
+ border: border(theme.highest, "accent", "pressed"),
+ },
+ },
+ },
+ }),
action_button: toggleable({
base: interactive({
base: {