use crate::InteractiveElement;
use crate::prelude::FluentBuilder as _;
use crate::styled_ext::Selectable;
use crate::theme::ActiveTheme as _;
use crate::{
AnyElement, App, Bounds, Button, ButtonVariants as _, Entity, Half, Hitbox, HitboxBehavior,
IconName, IntoElement, MouseButton, ParentElement as _, Pixels, Point, Refineable, RenderOnce,
Sizable as _, StatefulInteractiveElement, StyleRefinement, Styled, TextAlign, TextRun, Window,
div, point, px, size,
};
use super::super::blink_cursor::CURSOR_WIDTH;
use super::super::input::{FOLD_ICON_HITBOX_WIDTH, LINE_NUMBER_RIGHT_MARGIN};
use super::super::layout::LastLayout;
use super::super::rope_ext::RopeExt as _;
use super::super::{Input, InputState};
use super::inlay_hints::InlayHint;
use super::state::EditorState;
use crate::input_ui::InputContextMenuBuilder;
use crate::input_ui::{Enter, Escape, MoveDown, MoveUp};
const FOLD_ICON_WIDTH: Pixels = px(14.);
pub(crate) struct FoldIconLayout {
line_number_hitbox: Hitbox,
icons: Vec<(usize, bool, crate::AnyElement)>,
}
pub(crate) fn layout_fold_icons(
state: &Entity<InputState>,
origin_x: Pixels,
bounds: &Bounds<Pixels>,
last_layout: &LastLayout,
window: &mut Window,
cx: &mut App,
) -> FoldIconLayout {
struct FoldInfo {
buffer_line: usize,
is_folded: bool,
display_row: usize,
offset_y: Pixels,
}
let line_number_hitbox = window.insert_hitbox(
Bounds::new(
point(origin_x, bounds.origin.y + last_layout.visible_top),
size(last_layout.line_number_width, bounds.size.height),
),
HitboxBehavior::Normal,
);
let mut icon_layout = FoldIconLayout {
line_number_hitbox,
icons: vec![],
};
let fold_infos: Vec<FoldInfo> = {
let state = state.read(cx);
if !state.mode.is_folding() {
return icon_layout;
}
let mut infos = Vec::with_capacity(last_layout.visible_buffer_lines.len());
let mut offset_y = last_layout.visible_top;
for (line, &buffer_line) in last_layout
.lines
.iter()
.zip(last_layout.visible_buffer_lines.iter())
{
if state.display_map.is_fold_candidate(buffer_line) {
let is_folded = state.display_map.is_folded_at(buffer_line);
infos.push(FoldInfo {
buffer_line,
is_folded,
display_row: buffer_line,
offset_y,
});
}
offset_y += line.wrapped_lines.len() * last_layout.line_height;
}
infos
};
let line_height = last_layout.line_height;
let line_number_width =
last_layout.line_number_width - LINE_NUMBER_RIGHT_MARGIN - FOLD_ICON_HITBOX_WIDTH;
let icon_relative_pos = point(
(FOLD_ICON_HITBOX_WIDTH - FOLD_ICON_WIDTH).half(),
(line_height - FOLD_ICON_WIDTH).half(),
);
for (ix, info) in fold_infos.iter().enumerate() {
let fold_icon_bounds = Bounds::new(
point(
origin_x + icon_relative_pos.x + line_number_width,
bounds.origin.y + icon_relative_pos.y + info.offset_y,
),
size(FOLD_ICON_HITBOX_WIDTH, line_height),
);
let mut icon = Button::new(("fold", ix))
.ghost()
.icon(if info.is_folded {
IconName::ChevronRight
} else {
IconName::ChevronDown
})
.xsmall()
.rounded_xs()
.size(FOLD_ICON_WIDTH)
.selected(info.is_folded)
.on_mouse_down(MouseButton::Left, {
let state = state.clone();
let buffer_line = info.buffer_line;
move |_, _: &mut Window, cx: &mut App| {
cx.stop_propagation();
state.update(cx, |state, cx| {
state.display_map.toggle_fold(buffer_line);
cx.notify();
});
}
})
.into_any_element();
icon.prepaint_as_root(
fold_icon_bounds.origin,
fold_icon_bounds.size.into(),
window,
cx,
);
icon_layout
.icons
.push((info.display_row, info.is_folded, icon));
}
icon_layout
}
pub(crate) fn paint_fold_icons(
fold_icon_layout: &mut FoldIconLayout,
current_row: Option<usize>,
window: &mut Window,
cx: &mut App,
) {
let is_hovered = fold_icon_layout.line_number_hitbox.is_hovered(window);
for (display_row, is_folded, icon) in fold_icon_layout.icons.iter_mut() {
let is_current_line = current_row == Some(*display_row);
if !is_hovered && !is_current_line && !*is_folded {
continue;
}
icon.paint(window, cx);
}
}
pub(crate) fn extra_cursor_bounds(
state: &InputState,
caret_for: &dyn Fn(usize, usize, bool) -> Point<Pixels>,
bounds: &Bounds<Pixels>,
line_number_width: Pixels,
cursor_scroll_x: Pixels,
line_height: Pixels,
cursor_height: Pixels,
text_align: TextAlign,
) -> Vec<Bounds<Pixels>> {
let mut extra_cursor_bounds = Vec::new();
if !state.masked {
for extra in &state.extra_selections {
let end = extra.end.min(state.core.text.len());
let row = state.core.text.offset_to_point(end).row;
let pos = caret_for(row, end, false);
let x = bounds.left() + pos.x + line_number_width + cursor_scroll_x;
let x = if text_align == TextAlign::Right {
x.min(bounds.right() - CURSOR_WIDTH)
} else {
x
};
extra_cursor_bounds.push(Bounds::new(
point(
x,
bounds.top() + pos.y + ((line_height - cursor_height) / 2.),
),
size(CURSOR_WIDTH, cursor_height),
));
}
}
extra_cursor_bounds
}
pub(crate) fn paint_inlay_hints(
state: &Entity<InputState>,
last_layout: &LastLayout,
text_origin: Point<Pixels>,
line_height: Pixels,
window: &mut Window,
cx: &mut App,
) {
let (enabled, hints, text) = state.read_with(cx, |state, _| {
(
state.inlay_hints_enabled,
state.inlay_hints.clone(),
state.text().clone(),
)
});
if !enabled || hints.is_empty() {
return;
}
let mut by_row: std::collections::HashMap<usize, Vec<&InlayHint>> =
std::collections::HashMap::new();
for hint in &hints {
let offset = hint.offset.min(text.len());
let row = text.offset_to_point(offset).row;
by_row.entry(row).or_default().push(hint);
}
let style = window.text_style();
let font_size = style.font_size.to_pixels(window.rem_size());
let color = cx.theme().muted_foreground;
let mut y = px(0.);
for ((line_layout, &buffer_row), &row_start) in last_layout
.lines
.iter()
.zip(last_layout.visible_buffer_lines.iter())
.zip(last_layout.visible_line_byte_offsets.iter())
{
if let Some(row_hints) = by_row.get(&buffer_row) {
for hint in row_hints {
let offset = hint.offset.min(text.len());
let local = offset.saturating_sub(row_start);
let Some(pos) = line_layout.position_for_index(local, last_layout, true) else {
continue;
};
let first_line = hint.text.lines().next().unwrap_or("");
if first_line.is_empty() {
continue;
}
let shaped = window.text_system().shape_line(
first_line.to_string().into(),
font_size,
&[TextRun {
len: first_line.len(),
font: style.font(),
color,
background_color: None,
underline: None,
strikethrough: None,
}],
None,
);
let _ = shaped.paint(
point(text_origin.x + pos.x, text_origin.y + y + pos.y),
line_height,
TextAlign::Left,
None,
window,
cx,
);
}
}
y += line_layout.size(line_height).height;
}
}
#[derive(IntoElement)]
pub struct Editor {
editor: Entity<EditorState>,
context_menu_extra: Option<InputContextMenuBuilder>,
context_menu_override: Option<InputContextMenuBuilder>,
show_context_menu: Option<bool>,
font_size: Option<crate::Pixels>,
style: StyleRefinement,
}
impl Editor {
pub fn new(editor: &Entity<EditorState>) -> Self {
Self {
editor: editor.clone(),
context_menu_extra: None,
context_menu_override: None,
show_context_menu: None,
font_size: None,
style: StyleRefinement::default(),
}
}
pub fn context_menu_extra(
mut self,
builder: impl Fn(
crate::menu::PopupMenu,
Entity<InputState>,
&mut Window,
&mut App,
) -> crate::menu::PopupMenu
+ 'static,
) -> Self {
self.context_menu_extra = Some(std::rc::Rc::new(builder));
self
}
pub fn context_menu_override(
mut self,
builder: impl Fn(
crate::menu::PopupMenu,
Entity<InputState>,
&mut Window,
&mut App,
) -> crate::menu::PopupMenu
+ 'static,
) -> Self {
self.context_menu_override = Some(std::rc::Rc::new(builder));
self
}
pub fn show_context_menu(mut self, show: bool) -> Self {
self.show_context_menu = Some(show);
self
}
pub fn font_size(mut self, font_size: crate::Pixels) -> Self {
self.font_size = Some(font_size);
self
}
}
impl Styled for Editor {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
fn crumb_elements(
editor: &Entity<EditorState>,
stack: Vec<crate::highlight::DocumentSymbol>,
id_prefix: &'static str,
) -> Vec<AnyElement> {
let mut crumbs: Vec<AnyElement> = Vec::new();
let last = stack.len().saturating_sub(1);
for (ix, symbol) in stack.into_iter().enumerate() {
let editor = editor.clone();
crumbs.push(
div()
.id((id_prefix, ix))
.cursor_pointer()
.child(symbol.name.clone())
.on_click(move |_, _, cx| {
editor.update(cx, |state, cx| {
state.goto_symbol(&symbol, cx);
});
})
.into_any_element(),
);
if ix != last {
crumbs.push(div().child("›").into_any_element());
}
}
crumbs
}
impl RenderOnce for Editor {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let (input, stack, sticky_position) = self.editor.read_with(cx, |state, cx| {
let stack = if state.sticky_scroll_enabled() {
state.sticky_stack(cx)
} else {
Vec::new()
};
(state.input().clone(), stack, state.sticky_position())
});
let effective_font_size = self.font_size;
input.update(cx, |state, cx| {
state.set_font_size_override(effective_font_size, cx);
});
let editor = self.editor.clone();
let show_top_bar = matches!(sticky_position, super::sticky_scroll::StickyPosition::Top);
let muted = cx.theme().muted_foreground;
let sticky: Option<AnyElement> = if show_top_bar {
(!stack.is_empty()).then(|| {
div()
.flex_none()
.px(px(12.0))
.py(px(2.0))
.text_xs()
.text_color(muted)
.flex()
.flex_row()
.items_center()
.gap(px(4.0))
.children(crumb_elements(&editor, stack, "sticky-crumb"))
.into_any_element()
})
} else {
None
};
let user_style = self.style;
let mut input_el = Input::new(&input).flex_1();
if let Some(show) = self.show_context_menu {
input_el = input_el.show_context_menu(show);
}
if let Some(extra) = self.context_menu_extra {
input_el = input_el
.context_menu_extra(move |menu, state, window, cx| extra(menu, state, window, cx));
}
if let Some(override_builder) = self.context_menu_override {
input_el = input_el.context_menu_override(move |menu, state, window, cx| {
override_builder(menu, state, window, cx)
});
}
let minimap = self
.editor
.read_with(cx, |state, _| state.minimap_enabled());
let editor_for_minimap = self.editor.clone();
let editor_for_keys = self.editor.clone();
crate::v_flex()
.size_full()
.children(sticky)
.child(
div()
.relative()
.flex()
.flex_1()
.child(input_el)
.when(minimap, |this| {
this.child(super::minimap::render_minimap(&editor_for_minimap, cx))
}),
)
.capture_action({
let editor = editor_for_keys.clone();
move |_: &MoveUp, _: &mut Window, cx: &mut App| {
if editor.read(cx).completion_menu_active() {
editor.update(cx, |state, cx| state.select_previous_completion(cx));
cx.stop_propagation();
}
}
})
.capture_action({
let editor = editor_for_keys.clone();
move |_: &MoveDown, _: &mut Window, cx: &mut App| {
if editor.read(cx).completion_menu_active() {
editor.update(cx, |state, cx| state.select_next_completion(cx));
cx.stop_propagation();
}
}
})
.capture_action({
let editor = editor_for_keys.clone();
move |action: &Enter, window: &mut Window, cx: &mut App| {
if !action.secondary
&& !action.shift
&& editor.read(cx).completion_menu_active()
{
editor.update(cx, |state, cx| {
state.accept_completion(None, window, cx);
});
cx.stop_propagation();
}
}
})
.capture_action({
let editor = editor_for_keys;
move |_: &Escape, _: &mut Window, cx: &mut App| {
if editor.read(cx).completion_menu_active() {
editor.update(cx, |state, cx| state.dismiss_completion(cx));
cx.stop_propagation();
}
}
})
.capture_action({
let editor = self.editor.clone();
move |action: &super::vim::VimKey, window: &mut Window, cx: &mut App| {
editor.update(cx, |state, cx| {
state.vim_key(action.key.as_ref(), window, cx);
});
cx.stop_propagation();
}
})
.capture_key_down({
let editor = self.editor;
move |event: &crate::KeyDownEvent, window: &mut Window, cx: &mut App| {
if event.keystroke.key.as_str() == "escape" {
editor.update(cx, |state, cx| {
state.vim_escape(window, cx);
});
}
}
})
.map(|mut this| {
this.style().refine(&user_style);
this
})
}
}