Skip to content
Merged
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
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ byte-for-byte, so a round trip never loses work. **Smart objects** keep
their source pixels, so transforming one repeatedly costs no more quality
than transforming it once. Also PNG, JPEG, WebP and TIFF, plus HEIC/HEIF import (iPhone photos)
and camera raw import: NEF, ARW, CR2, DNG, RAF, ORF, RW2, PEF, SRW and the
rest, developed with the camera's white balance to 16-bit sRGB. Raws
rest. A capture opens in Camera Raw with sensor-domain white balance and
exposure, live fast-demosaic previews and a best-quality render on Apply;
the original capture and all 15 development settings stay attached to the
layer and survive PSD/PSB save and reopen, so adjustments never compound on
the previously developed pixels. The rendered document remains 16-bit sRGB. Raws
decode through Schist's own clean-room `schist-codec-raw` crate (pure
Rust, written from the public specifications and verified sample for
sample against LibRaw across some 250 camera files), covering every
Expand Down
53 changes: 49 additions & 4 deletions crates/app/src/dialogs/filters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,16 @@ pub(super) fn filter_dialog(
map: Option<std::sync::Arc<schist_plugin_api::FilterImage>>,
cx: &mut Context<Workspace>,
) -> impl IntoElement {
let (name, specs) = ws
let raw_development = ws.is_raw_redevelopment(id);
let (mut name, specs) = ws
.registry
.filters()
.find(|f| f.id() == id)
.map(|f| (f.name().to_string(), f.params()))
.unwrap_or_else(|| (id.to_string(), Vec::new()));
if raw_development {
name = "Camera Raw Development".to_string();
}

// Scrolls, because Custom is a five-by-five kernel and Lighting
// Effects has a dozen sliders: a filter dialog is a list of whatever
Expand Down Expand Up @@ -61,6 +65,34 @@ pub(super) fn filter_dialog(
cx,
));
}
if raw_development {
body = body.child(ui::button(
"Reset to As Shot",
false,
move |ws, _window, cx| {
let Some(filter) = ws.registry.filters().find(|filter| filter.id() == id) else {
return;
};
let defaults = schist_plugin_api::FilterValues::defaults(&filter.params());
let mut next = None;
ws.update_modal(|modal| {
if let Modal::Filter {
values, preview, ..
} = modal
{
*values = defaults.clone();
if *preview {
next = Some(values.clone());
}
}
});
if let Some(values) = next {
ws.preview_filter(id, Some(&values), cx);
}
},
cx,
));
}
// A filter that takes an image gets a row to choose one with. This
// is Photoshop's "Choose a displacement map" dialog, except that it
// opens from inside the filter rather than in front of it, so the
Expand Down Expand Up @@ -149,7 +181,11 @@ pub(super) fn filter_dialog(
div()
.text_size(px(11.0))
.text_color(gpui::rgb(ui::palette().text_dim))
.child("Applies to the active layer, inside the selection."),
.child(if raw_development {
"Re-develops the active layer from its original sensor data."
} else {
"Applies to the active layer, inside the selection."
}),
);

let apply_values = values.clone();
Expand All @@ -167,8 +203,17 @@ pub(super) fn filter_dialog(
"OK",
true,
move |ws, _w, cx| {
ws.apply_filter(id, &apply_values, cx);
ws.close_modal(cx);
// A RAW-backed layer renders asynchronously. Close its
// preview dialog first, then let `apply_filter` replace it
// with a progress modal; the ordinary pixel-filter path
// remains synchronous and closes afterwards.
if ws.is_raw_redevelopment(id) {
ws.close_modal(cx);
ws.apply_filter(id, &apply_values, cx);
} else {
ws.apply_filter(id, &apply_values, cx);
ws.close_modal(cx);
}
},
cx,
));
Expand Down
12 changes: 5 additions & 7 deletions crates/app/src/native_menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ pub fn sync(ws: &mut Workspace, cx: &mut Context<Workspace>) {
fn signature(ws: &Workspace) -> String {
let v = &ws.view;
let mut out = format!(
"{}{}{}{}{}{}{}",
"{}{}{}{}{}{}{}{}",
v.rulers as u8,
v.grid as u8,
v.guides as u8,
Expand All @@ -45,6 +45,9 @@ fn signature(ws: &Workspace) -> String {
ws.color.proof.is_some() as u8,
// The gallery swaps the whole menu set out.
ws.gallery_open() as u8,
// Camera Raw changes its label when the active layer carries an
// original capture, so switching documents/layers must rebuild it.
ws.is_raw_redevelopment("filter.camera_raw") as u8,
);
if let Some(doc) = ws.doc.as_ref() {
for comp in &doc.layer_comps {
Expand Down Expand Up @@ -133,12 +136,7 @@ fn item(ws: &Workspace, entry: MenuEntry) -> Option<MenuItem> {
}),
),
MenuEntry::Filter(id) => {
let name = ws
.registry
.filters()
.find(|f| f.id() == id)
.map(|f| format!("{}…", f.name()))
.unwrap_or_else(|| id.to_string());
let name = panels::filter_menu_label(ws, id);
action_item(name, Box::new(OpenFilter { id: id.to_string() }))
}
MenuEntry::Dynamic(label, item) => action_item(label, Box::new(RunAppItem { item })),
Expand Down
7 changes: 1 addition & 6 deletions crates/app/src/panels/menu_bar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -508,12 +508,7 @@ pub(super) fn menu_entry_row(
)
.into_any_element(),
MenuEntry::Filter(id) => {
let name = ws
.registry
.filters()
.find(|f| f.id() == id)
.map(|f| format!("{}…", f.name()))
.unwrap_or_else(|| id.to_string());
let name = filter_menu_label(ws, id);
menu_row(
name,
String::new(),
Expand Down
16 changes: 16 additions & 0 deletions crates/app/src/panels/menus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,22 @@ pub(crate) enum MenuEntry {
Sep,
}

/// A RAW-backed layer uses Camera Raw as a non-destructive development
/// workflow; everywhere else it remains the ordinary destructive filter.
/// Keep the menu label in step with the dialog title for both menu-bar
/// implementations.
pub(crate) fn filter_menu_label(ws: &Workspace, id: &str) -> String {
if ws.is_raw_redevelopment(id) {
"Camera Raw Development…".to_string()
} else {
ws.registry
.filters()
.find(|filter| filter.id() == id)
.map(|filter| format!("{}…", filter.name()))
.unwrap_or_else(|| id.to_string())
}
}

pub(crate) fn menus(ws: &Workspace) -> Vec<(&'static str, Vec<MenuEntry>)> {
use AppItem::*;
use MenuEntry::*;
Expand Down
10 changes: 8 additions & 2 deletions crates/app/src/workspace/adjustments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,19 @@ impl Workspace {
let Some(filter) = self.registry.filters().find(|f| f.id() == id) else {
return;
};
let values = schist_plugin_api::FilterValues::defaults(&filter.params());
let mut values = schist_plugin_api::FilterValues::defaults(&filter.params());
self.seed_raw_filter_values(id, &mut values);
// Filters with no parameters just run.
if values.0.is_empty() {
self.apply_filter(id, &values, cx);
return;
}
if !self.begin_filter_preview() {
let begun = if self.is_raw_redevelopment(id) {
self.begin_raw_filter_preview()
} else {
self.begin_filter_preview()
};
if !begun {
cx.notify();
return;
}
Expand Down
17 changes: 17 additions & 0 deletions crates/app/src/workspace/docs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,20 @@ impl Workspace {
let _ = &path;
match result {
Ok(doc) => {
// A capture opens into its development workflow. A PSD/PSB
// that happens to contain a RAW-backed layer does not: it is
// already an edited document and should reopen undisturbed.
let raw_capture = path
.extension()
.and_then(|ext| ext.to_str())
.map(str::to_ascii_lowercase)
.is_some_and(|ext| {
schist_codecs_common::raw::RAW_EXTENSIONS.contains(&ext.as_str())
})
&& doc
.active_layer
.and_then(|id| doc.tree.find(id))
.is_some_and(|layer| layer.raw.is_some());
self.status = match &doc.path {
Some(p) => format!("Opened {}", p.display()).into(),
None => format!("Opened {}", doc.title).into(),
Expand All @@ -378,6 +392,9 @@ impl Workspace {
#[cfg(not(target_arch = "wasm32"))]
self.finish_load_bookkeeping(&path);
self.offer_missing_fonts(cx);
if raw_capture {
self.open_filter_dialog("filter.camera_raw", cx);
}
}
// A HEIC on a machine with no libheif — or a libheif with
// no HEVC decoder, as stock Ubuntu ships: downloading the
Expand Down
Loading
Loading