Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
> [!IMPORTANT]
> Remove this line to confirm you've reviewed this PR before submitting.
# Zed

[![Zed](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/zed-industries/zed/main/assets/badge/v0.json)](https://zed.dev)
Expand Down
19 changes: 19 additions & 0 deletions assets/settings/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,25 @@
// The maximum width, in pixels, of the rendered markdown content when
// limit_content_width is enabled.
"max_width": 800,
// Whether to constrain top-level Mermaid blocks to `mermaid_max_width`.
// When disabled, no Mermaid-specific maximum width is applied.
// To restore Zed's native layout, disable both Mermaid width options
// and set mermaid_alignment to "left".
"limit_mermaid_width": false,
// The maximum width, in pixels, of top-level Mermaid blocks when
// `limit_mermaid_width` is enabled and `mermaid_width_follows_diagram`
// is disabled. Has no effect otherwise.
"mermaid_max_width": 800,
// Whether top-level Mermaid blocks follow the diagram's 100% natural
// width, with enough space for controls, instead of using Zed's default
// full-width Mermaid block. This takes precedence over
// `limit_mermaid_width`. Interactive zoom remains within the block.
"mermaid_width_follows_diagram": false,
// Where to align top-level Mermaid diagrams horizontally: "left",
// "center", or "right". In the default or limited-width layout this
// aligns the diagram within the Mermaid block; when
// `mermaid_width_follows_diagram` is enabled, it aligns the block itself.
"mermaid_alignment": "center",
},
// Determines the modifier to be used to add multiple cursors with the mouse. The open hover link mouse gestures will adapt such that it do not conflict with the multicursor modifier.
//
Expand Down
39 changes: 39 additions & 0 deletions crates/gpui/src/elements/div.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1384,6 +1384,15 @@ pub trait StatefulInteractiveElement: InteractiveElement {
self
}

/// Set the disabled state reported to assistive technology.
///
/// This only changes accessibility metadata; input handlers must enforce
/// the disabled state separately.
fn aria_disabled(mut self, disabled: bool) -> Self {
self.interactivity().aria.disabled = Some(disabled);
self
}

/// Set the selected state for this element.
fn aria_selected(mut self, selected: bool) -> Self {
self.interactivity().aria.selected = Some(selected);
Expand Down Expand Up @@ -2102,6 +2111,7 @@ pub(crate) struct AriaProperties {
pub(crate) label: Option<SharedString>,
pub(crate) description: Option<SharedString>,
pub(crate) keyshortcuts: Option<SharedString>,
pub(crate) disabled: Option<bool>,
pub(crate) selected: Option<bool>,
pub(crate) expanded: Option<bool>,
pub(crate) toggled: Option<accesskit::Toggled>,
Expand Down Expand Up @@ -3534,6 +3544,13 @@ impl Interactivity {
if let Some(keyshortcuts) = &self.aria.keyshortcuts {
node.set_keyboard_shortcut(keyshortcuts.to_string());
}
if let Some(disabled) = self.aria.disabled {
if disabled {
node.set_disabled();
} else {
node.clear_disabled();
}
}
if let Some(selected) = self.aria.selected {
node.set_selected(selected);
}
Expand Down Expand Up @@ -5256,6 +5273,28 @@ mod tests {
assert_eq!(node.author_id(), Some("settings.buffer-font-size"));
}

#[test]
fn test_aria_disabled_preserves_default_and_can_be_cleared() {
for (disabled, initially_disabled, expected_disabled) in [
(None, false, false),
(None, true, true),
(Some(true), false, true),
(Some(false), true, false),
] {
let element = div()
.id("number-field")
.when_some(disabled, |this, disabled| this.aria_disabled(disabled));
let mut node = accesskit::Node::new(accesskit::Role::SpinButton);
if initially_disabled {
node.set_disabled();
}

element.write_a11y_info(&mut node);

assert_eq!(node.is_disabled(), expected_disabled);
}
}

#[test]
fn test_write_a11y_info_string_and_numeric_properties() {
let mut interactivity = Interactivity::default();
Expand Down
156 changes: 156 additions & 0 deletions crates/markdown/src/markdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1717,6 +1717,79 @@ pub enum AutoscrollBehavior {
Controlled(ScrollHandle),
}

/// Where a Mermaid diagram is aligned horizontally.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum MermaidAlignment {
/// Align the diagram to the left.
#[default]
Left,
/// Center the diagram.
Center,
/// Align the diagram to the right.
Right,
}

/// Layout overrides for top-level Mermaid diagrams.
///
/// These defaults describe Zed's native renderer, independently of the
/// Markdown Preview preference defaults. Nested diagrams use this baseline
/// to retain their parent layout.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct MermaidLayout {
/// Optional Mermaid-specific maximum width for the block.
///
/// `None` means no Mermaid-specific width override; Zed's native Markdown
/// and Mermaid layout remains responsible for the block width.
///
/// Ignored when `width_follows_diagram` is enabled.
pub max_width: Option<Pixels>,
/// Horizontal alignment for the Mermaid diagram or, when
/// `width_follows_diagram` is enabled, for the Mermaid block itself.
pub alignment: MermaidAlignment,
/// When true, the top-level Mermaid block follows the rendered diagram's
/// 100% natural width instead of using Zed's native full-width block layout,
/// with enough space for its controls.
///
/// This takes precedence over `max_width`. Interactive zoom changes the
/// diagram inside the block without changing the block's baseline width.
pub width_follows_diagram: bool,
}

impl MermaidLayout {
pub fn has_width_override(&self) -> bool {
self.max_width.is_some() || self.width_follows_diagram
}

/// Whether any Mermaid layout override is active. When this
/// is `false`, the Mermaid preview uses Zed's native layout unchanged.
pub fn has_overrides(&self) -> bool {
self.has_width_override() || self.alignment != MermaidAlignment::Left
}
}

/// How a top-level block participates in the content width limit.
#[derive(Clone, Copy, Debug, Default)]
enum RootBlockWidth {
/// Constrain to `content_max_width`, like normal content.
#[default]
Default,
/// Render at the full width of the container.
Full,
/// Constrain to this width, ignoring `content_max_width`.
Fixed(Pixels),
}

/// Constrains a top-level block to `max_width`, centering it within the
/// container.
fn constrain_width(child: AnyElement, max_width: Pixels) -> AnyElement {
div()
.w_full()
.max_w(max_width)
.mx_auto()
.child(child)
.into_any_element()
}

pub struct MarkdownElement {
markdown: Entity<Markdown>,
style: MarkdownStyle,
Expand All @@ -1730,6 +1803,12 @@ pub struct MarkdownElement {
on_mermaid_zoom: Option<MermaidZoomCallback>,
image_resolver: Option<Box<dyn Fn(&str, &App) -> Option<ImageSource>>>,
show_root_block_markers: bool,
/// When set, top-level blocks are constrained to this width and centered.
/// Blocks may opt out via the internal root block width, e.g. Mermaid
/// diagrams that are configured to be wider than the content.
content_max_width: Option<Pixels>,
/// How Mermaid diagrams are laid out.
mermaid_layout: MermaidLayout,
autoscroll: AutoscrollBehavior,
/// Test-only hook to observe the laid-out text when this element is
/// rendered beneath a view, where the layout state isn't otherwise
Expand Down Expand Up @@ -1757,6 +1836,8 @@ impl MarkdownElement {
on_mermaid_zoom: None,
image_resolver: None,
show_root_block_markers: false,
content_max_width: None,
mermaid_layout: MermaidLayout::default(),
autoscroll: AutoscrollBehavior::Propagate,
#[cfg(test)]
on_render: None,
Expand Down Expand Up @@ -1863,6 +1944,20 @@ impl MarkdownElement {
self
}

/// Constrains top-level Markdown blocks to `max_width` and centers them,
/// while wide blocks (e.g. Mermaid diagrams) are allowed to use the full
/// width of the container. Passing `None` renders all blocks edge to edge.
pub fn content_max_width(mut self, max_width: Option<Pixels>) -> Self {
self.content_max_width = max_width;
self
}

/// Configures how Mermaid diagrams are laid out.
pub fn mermaid_layout(mut self, mermaid_layout: MermaidLayout) -> Self {
self.mermaid_layout = mermaid_layout;
self
}

pub fn scroll_handle(mut self, scroll_handle: ScrollHandle) -> Self {
self.autoscroll = AutoscrollBehavior::Controlled(scroll_handle);
self
Expand Down Expand Up @@ -2616,6 +2711,8 @@ impl Element for MarkdownElement {
self.style.syntax.clone(),
highlights,
parsed_markdown.code_block_highlights.clone(),
self.content_max_width,
self.mermaid_layout,
);
let markdown_end = if let Some(last) = parsed_markdown.events.last() {
last.0.end
Expand Down Expand Up @@ -2660,6 +2757,7 @@ impl Element for MarkdownElement {

match event {
MarkdownEvent::RootStart => {
builder.root_block_width = RootBlockWidth::Default;
if self.show_root_block_markers {
builder.push_root_block(range, markdown_end);
}
Expand Down Expand Up @@ -2760,6 +2858,31 @@ impl Element for MarkdownElement {
} => *copy_button_visibility,
_ => CopyButtonVisibility::VisibleOnHover,
};
// Top-level Mermaid blocks only override their
// width when a Mermaid layout option is active:
// width-following releases the block, the width
// limit pins it to `mermaid_max_width`, and
// otherwise the block keeps Zed's native layout.
// Nested diagrams stay within their parent block.
let is_top_level =
builder.div_stack.len() == builder.root_block_content_depth;
let mermaid_layout = if is_top_level {
builder.mermaid_layout
} else {
MermaidLayout::default()
};
if is_top_level {
builder.root_block_width = match (
mermaid_layout.width_follows_diagram,
mermaid_layout.max_width,
) {
(true, _) => RootBlockWidth::Full,
(false, Some(max_width)) => {
RootBlockWidth::Fixed(max_width)
}
(false, None) => RootBlockWidth::Default,
};
}
builder.push_sourced_element(
mermaid_diagram.content_range.clone(),
render_mermaid_diagram(
Expand All @@ -2771,6 +2894,7 @@ impl Element for MarkdownElement {
showing_code,
zoom,
copy_button_visibility,
mermaid_layout,
self.on_mermaid_zoom.clone(),
window,
cx,
Expand Down Expand Up @@ -3708,6 +3832,16 @@ struct MarkdownElementBuilder {
table: TableState,
syntax_theme: Arc<SyntaxTheme>,
highlights: MarkdownHighlights,
/// See `MarkdownElement::content_max_width`.
content_max_width: Option<Pixels>,
/// How the current root block should be constrained. Reset at each root
/// block boundary.
root_block_width: RootBlockWidth,
/// Depth of the div holding a root block's contents, used to tell whether a
/// block is top-level and therefore eligible to be a wide block.
root_block_content_depth: usize,
/// How Mermaid diagrams are laid out.
mermaid_layout: MermaidLayout,
}

struct MarkdownHighlights {
Expand Down Expand Up @@ -3805,6 +3939,8 @@ impl MarkdownElementBuilder {
syntax_theme: Arc<SyntaxTheme>,
highlights: MarkdownHighlights,
code_block_highlights: Arc<CodeBlockHighlights>,
content_max_width: Option<Pixels>,
mermaid_layout: MermaidLayout,
) -> Self {
Self {
div_stack: vec![{
Expand All @@ -3829,6 +3965,10 @@ impl MarkdownElementBuilder {
table: TableState::default(),
syntax_theme,
highlights,
content_max_width,
root_block_width: RootBlockWidth::Default,
root_block_content_depth: 1,
mermaid_layout,
}
}

Expand Down Expand Up @@ -3905,6 +4045,7 @@ impl MarkdownElementBuilder {
markdown_end,
);
self.push_div(div().pl_4(), range, markdown_end);
self.root_block_content_depth = self.div_stack.len();
}

fn push_image_child(&mut self, child: impl IntoElement) {
Expand All @@ -3931,6 +4072,20 @@ impl MarkdownElementBuilder {
}

fn append_child(&mut self, child: AnyElement) {
// Only direct children of the root container are top-level blocks, and
// they are the ones subject to the content width limit.
let child = if self.div_stack.len() == 1 {
match self.root_block_width {
RootBlockWidth::Full => child,
RootBlockWidth::Fixed(max_width) => constrain_width(child, max_width),
RootBlockWidth::Default => match self.content_max_width {
Some(max_width) => constrain_width(child, max_width),
None => child,
},
}
} else {
child
};
self.div_stack.last_mut().unwrap().div.extend([child]);
}

Expand Down Expand Up @@ -3975,6 +4130,7 @@ impl MarkdownElementBuilder {
)
});
self.pop_div();
self.root_block_content_depth = self.div_stack.len();
}

fn pop_div(&mut self) {
Expand Down
Loading
Loading