Skip to content
Draft
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
1 change: 1 addition & 0 deletions crates/mobile/src/visual/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1273,6 +1273,7 @@ impl Worker {
blend: node.blend,
bloom: node.bloom,
textures: &textures,
fade_edges: true,
},
&uniforms,
size.0,
Expand Down
9 changes: 8 additions & 1 deletion crates/ui/examples/new-project-fixture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ fn main() -> anyhow::Result<()> {
adjustment: settings::NewThreadBackgroundAdjustment::default(),
});
}
if let Ok(path) = std::env::var("ZERON_FIXTURE_GENERATIVE") {
settings.new_thread_generative_art = Some(std::fs::read_to_string(path).unwrap());
settings.new_thread_background_source = settings::NewThreadBackgroundSource::Generative;
}
if let Ok(path) = std::env::var("ZERON_FIXTURE_HOME_VISUAL") {
settings.new_thread_visual = Some(std::fs::read_to_string(path).unwrap());
}
if let Some(folder) = std::env::var_os("ZERON_FIXTURE_WALLPAPER_FOLDER") {
settings.wallpaper_folder = Some(folder.into());
}
Expand Down Expand Up @@ -47,7 +54,7 @@ fn main() -> anyhow::Result<()> {
{"id":"remote","name":"Build server","platform":"linux","lastSeenAt":null},
{"id":"laptop","name":"Travel laptop","platform":"macos","lastSeenAt":null}
])).unwrap();
s.selected_chat = if std::env::var_os("ZERON_FIXTURE_BACKGROUND").is_some() { None } else { Some("browser-fixture".into()) }; s.selected_space = Some("project".into());
s.selected_chat = if std::env::var_os("ZERON_FIXTURE_BACKGROUND").is_some() || std::env::var_os("ZERON_FIXTURE_HOME_VISUAL").is_some() || std::env::var_os("ZERON_FIXTURE_GENERATIVE").is_some() { None } else { Some("browser-fixture".into()) }; s.selected_space = Some("project".into());
s.auto_selected = true; s.chats_synced = true; s.spaces_synced = true;
s.spaces = vec![serde_json::from_value(serde_json::json!({"id":"project","deviceId":"local","path":"/tmp/fieldnotes","createdAt":"2026-09-08T00:00:00Z"})).unwrap()];
s.chats = vec![serde_json::from_value(serde_json::json!({"id":"browser-fixture","deviceId":"local","spaceId":"project","title":"Build the Fieldnotes workspace","archived":false,"createdAt":"2026-09-08T00:00:00Z","config":{"harness":"claude-code","model":"claude-sonnet-4-6","reasoning":null,"sandbox":"workspace-write"}})).unwrap()];
Expand Down
20 changes: 19 additions & 1 deletion crates/ui/src/composer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1530,6 +1530,8 @@ enum EditKind {
const GENERIC_COMPOSER_CONTEXT: &str = "Composer";
pub(crate) const MESSAGE_COMPOSER_CONTEXT: &str = "MessageComposer";
const PALETTE_SEARCH_CONTEXT: &str = "PaletteSearch";
/// Code fields: Enter breaks the line; nothing submits.
pub(crate) const CODE_INPUT_CONTEXT: &str = "CodeInput";

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum MessageEnterBindingAction {
Expand Down Expand Up @@ -1776,6 +1778,9 @@ pub fn init(cx: &mut App, send_behavior: ComposerSendBehavior) {
}
cx.bind_keys(palette_bindings);
cx.bind_keys(generic_bindings);
let mut code_bindings = input_bindings(CODE_INPUT_CONTEXT);
code_bindings.push(KeyBinding::new("enter", Newline, Some(CODE_INPUT_CONTEXT)));
cx.bind_keys(code_bindings);
cx.bind_keys(message_bindings);
}

Expand Down Expand Up @@ -1979,6 +1984,8 @@ pub struct ComposerInput {
text_size: f32,
configured_line_height: f32,
single_line: bool,
/// Set in the monospace face (code fields).
monospace: bool,
scroll_left: f32,
// -- measured state (written during layout/paint) --
last_lines: Vec<WrappedLine>,
Expand Down Expand Up @@ -2097,6 +2104,7 @@ impl ComposerInput {
text_size: INPUT_TEXT_SIZE,
configured_line_height: INPUT_LINE_HEIGHT,
single_line: false,
monospace: false,
scroll_left: 0.0,
last_lines: Vec::new(),
line_starts: vec![0],
Expand Down Expand Up @@ -2161,6 +2169,12 @@ impl ComposerInput {
self
}

/// Set the text in the monospace face, for code.
pub(crate) fn with_monospace(mut self) -> Self {
self.monospace = true;
self
}

/// Join the window's Tab order, for a field the keyboard must reach.
pub(crate) fn with_tab_stop(mut self) -> Self {
self.focus_handle = self.focus_handle.tab_stop(true);
Expand Down Expand Up @@ -5311,7 +5325,11 @@ impl Render for ComposerInput {
.text_size(crate::typography::ui_rems(self.text_size))
.line_height(crate::typography::ui_rems(self.configured_line_height))
.text_color(text_color)
.font_family(theme.font_sans.clone())
.font_family(if self.monospace {
theme.font_mono.clone()
} else {
theme.font_sans.clone()
})
.child({
let input = cx.entity();
let ascent = self.max_ascent;
Expand Down
2 changes: 2 additions & 0 deletions crates/ui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ pub mod motion;
mod new_thread_background_effects;
mod new_thread_background_image;
mod new_thread_background_mask;
mod new_thread_generative;
mod notice;
pub mod notify;
pub mod orb;
Expand All @@ -66,6 +67,7 @@ pub mod rail;
mod roll_text;
pub mod running_pill;
pub mod settings;
mod shader_player;
pub mod shell;
pub mod sound;
pub mod state;
Expand Down
45 changes: 45 additions & 0 deletions crates/ui/src/new_thread_background_effects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,17 @@ fn crossfade_mix(start: Instant, now: Instant) -> f32 {
}

impl Readiness {
/// A live source (the generative background, keyed by `path`) replaces
/// its own frames in place, before [`Readiness::frame`] runs: only
/// switching to or from it crossfades, including mid-crossfade.
pub fn adopt_live_frame(&mut self, image: Option<&Arc<gpui::RenderImage>>, path: &Path) {
if let Some(image) = image
&& self.current_path.as_deref() == Some(path)
{
self.current = Some(image.clone());
}
}

pub fn frame(
&mut self,
image: Option<Arc<gpui::RenderImage>>,
Expand Down Expand Up @@ -469,6 +480,40 @@ mod tests {
assert!(removed.current.is_none() && removed.previous.is_none());
}

#[test]
fn live_frames_replace_in_place_and_only_switching_sources_crossfades() {
let image = artwork();
let (first, second, third) = (artwork(), artwork(), artwork());
let mut ready = Readiness::default();
let now = Instant::now();
let adjustment = NewThreadBackgroundAdjustment::default();
let image_path = Path::new("background.png");
let live = Path::new("generative:1");
ready.frame(Some(image.clone()), Some(image_path), adjustment, true, true, now);
// Switching to the live source crossfades from the image.
ready.adopt_live_frame(Some(&first), live);
let start = ready.frame(Some(first.clone()), Some(live), adjustment, true, false, now);
assert_eq!(start.previous.unwrap().id, image.id);
assert_eq!(start.current.unwrap().id, first.id);
// Its next frame lands mid-crossfade: shown at once, still fading in.
ready.adopt_live_frame(Some(&second), live);
let mid = ready.frame(Some(second.clone()), Some(live), adjustment, true, false, now);
assert_eq!(mid.current.unwrap().id, second.id);
assert_eq!(mid.previous.unwrap().id, image.id);
assert!(mid.active);
// After it, frames replace each other without starting another.
let done = now + std::time::Duration::from_secs(1);
ready.frame(Some(second), Some(live), adjustment, true, false, done);
ready.adopt_live_frame(Some(&third), live);
let later = ready.frame(Some(third.clone()), Some(live), adjustment, true, false, done);
assert_eq!(later.current.unwrap().id, third.id);
assert!(later.previous.is_none() && !later.active);
// A frame of another live key doesn't hijack the current source.
ready.adopt_live_frame(Some(&image), Path::new("generative:2"));
let held = ready.frame(None, Some(live), adjustment, true, false, done);
assert_eq!(held.current.unwrap().id, third.id);
}

#[test]
fn wallpaper_replacement_and_removal_keep_the_departing_crop() {
let first = artwork();
Expand Down
Loading
Loading