use adw::prelude::*; use gtk::glib; use std::cell::Cell; use std::rc::Rc; use crate::app::AppState; fn get_first_image_aspect(files: &[std::path::PathBuf]) -> f64 { let Some(first) = files.first() else { return 0.0 }; let Ok((w, h)) = image::image_dimensions(first) else { return 0.0 }; if h == 0 { return 0.0; } w as f64 / h as f64 } fn get_image_dims(path: &std::path::Path) -> (u32, u32) { image::image_dimensions(path).unwrap_or((4000, 3000)) } fn get_first_image_dims(files: &[std::path::PathBuf]) -> (u32, u32) { let Some(first) = files.first() else { return (4000, 3000) }; get_image_dims(first) } fn algo_index_to_filter(idx: u32) -> image::imageops::FilterType { match idx { 1 => image::imageops::FilterType::CatmullRom, 2 => image::imageops::FilterType::Triangle, 3 => image::imageops::FilterType::Nearest, _ => image::imageops::FilterType::Lanczos3, } } const CATEGORIES: &[&str] = &[ "Fediverse / Open Platforms", "Mainstream Platforms", "Common / Web", ]; fn presets_for_category(cat: u32) -> &'static [(&'static str, u32, u32)] { match cat { 0 => &[ ("Mastodon Post (1920 x 1080)", 1920, 1080), ("Mastodon Profile (400 x 400)", 400, 400), ("Mastodon Header (1500 x 500)", 1500, 500), ("Pixelfed Post (1080 x 1080)", 1080, 1080), ("Pixelfed Story (1080 x 1920)", 1080, 1920), ("Bluesky Post (1200 x 630)", 1200, 630), ("Bluesky Profile (400 x 400)", 400, 400), ("Bluesky Banner (1500 x 500)", 1500, 500), ("Lemmy Post (1200 x 630)", 1200, 630), ("PeerTube Thumbnail (1280 x 720)", 1280, 720), ("Friendica Post (1200 x 630)", 1200, 630), ("Funkwhale Cover (1400 x 1400)", 1400, 1400), ], 1 => &[ ("Instagram Post Square (1080 x 1080)", 1080, 1080), ("Instagram Post Portrait (1080 x 1350)", 1080, 1350), ("Instagram Story/Reel (1080 x 1920)", 1080, 1920), ("Facebook Post (1200 x 630)", 1200, 630), ("Facebook Cover (820 x 312)", 820, 312), ("Facebook Profile (170 x 170)", 170, 170), ("YouTube Thumbnail (1280 x 720)", 1280, 720), ("YouTube Channel Art (2560 x 1440)", 2560, 1440), ("LinkedIn Post (1200 x 627)", 1200, 627), ("LinkedIn Cover (1584 x 396)", 1584, 396), ("LinkedIn Profile (400 x 400)", 400, 400), ("Pinterest Pin (1000 x 1500)", 1000, 1500), ("TikTok Video Cover (1080 x 1920)", 1080, 1920), ("Threads Post (1080 x 1080)", 1080, 1080), ], _ => &[ ("4K UHD (3840 x 2160)", 3840, 2160), ("Full HD (1920 x 1080)", 1920, 1080), ("HD Ready (1280 x 720)", 1280, 720), ("Blog Standard (800 wide)", 800, 0), ("Email Header (600 x 200)", 600, 200), ("Large Thumbnail (300 x 300)", 300, 300), ("Small Thumbnail (150 x 150)", 150, 150), ("Favicon (32 x 32)", 32, 32), ], } } fn rebuild_size_model(size_row: &adw::ComboRow, cat: u32) { let presets = presets_for_category(cat); let mut names: Vec<&str> = vec!["(select a size)"]; names.extend(presets.iter().map(|p| p.0)); size_row.set_model(Some(>k::StringList::new(&names))); size_row.set_list_factory(Some(&super::full_text_list_factory())); size_row.set_selected(0); } pub fn build_resize_page(state: &AppState) -> adw::NavigationPage { let cfg = state.job_config.borrow(); // === OUTER LAYOUT === let outer = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .spacing(0) .vexpand(true) .build(); // --- Enable toggle (full width) --- let enable_group = adw::PreferencesGroup::builder() .margin_start(12) .margin_end(12) .margin_top(12) .build(); let enable_row = adw::SwitchRow::builder() .title("Enable Resize") .subtitle("Scale images to new dimensions") .active(cfg.resize_enabled) .tooltip_text("Toggle resizing of images on or off") .build(); enable_group.add(&enable_row); outer.append(&enable_group); // --- Horizontal split: Preview (left 60%) | Controls (right 40%) --- let split = gtk::Box::builder() .orientation(gtk::Orientation::Horizontal) .spacing(12) .margin_start(12) .margin_end(12) .margin_top(12) .margin_bottom(12) .vexpand(true) .build(); // ========== LEFT: Preview ========== let preview_box = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .spacing(8) .hexpand(true) .valign(gtk::Align::Start) .build(); let thumb_picture = gtk::Picture::builder() .content_fit(gtk::ContentFit::Contain) .height_request(250) .hexpand(true) .build(); thumb_picture.add_css_class("card"); thumb_picture.update_property(&[ gtk::accessible::Property::Label("Resize preview - click to cycle images"), ]); let dims_label = gtk::Label::builder() .css_classes(["dim-label", "caption"]) .halign(gtk::Align::Center) .margin_top(4) .build(); let no_preview_label = gtk::Label::builder() .label("Add images to see resize preview") .css_classes(["dim-label"]) .halign(gtk::Align::Center) .build(); preview_box.append(&thumb_picture); preview_box.append(&dims_label); preview_box.append(&no_preview_label); // ========== RIGHT: Controls ========== let controls_scroll = gtk::ScrolledWindow::builder() .hscrollbar_policy(gtk::PolicyType::Never) .vexpand(true) .width_request(340) .build(); let controls = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .spacing(12) .build(); // --- Group 1: Preset (two-level: category then size) --- let preset_group = adw::PreferencesGroup::builder() .title("Preset") .description("Pick a category, then a size") .build(); let category_row = adw::ComboRow::builder() .title("Category") .use_subtitle(true) .tooltip_text("Choose a category of size presets") .build(); category_row.set_model(Some(>k::StringList::new(CATEGORIES))); category_row.set_list_factory(Some(&super::full_text_list_factory())); let size_row = adw::ComboRow::builder() .title("Size") .subtitle("Select a preset to fill dimensions") .use_subtitle(true) .tooltip_text("Pick a preset size to fill dimensions") .build(); rebuild_size_model(&size_row, 0); preset_group.add(&category_row); preset_group.add(&size_row); controls.append(&preset_group); // --- Group 2: Dimensions --- let dims_group = adw::PreferencesGroup::builder() .title("Dimensions") .build(); // Custom horizontal row: [W] [width_spin] [lock] [height_spin] [H] [px|%] let dim_row = gtk::Box::builder() .orientation(gtk::Orientation::Horizontal) .spacing(6) .margin_start(12) .margin_end(12) .margin_top(10) .margin_bottom(10) .halign(gtk::Align::Center) .build(); let w_label = gtk::Label::builder() .label("W") .css_classes(["dim-label"]) .build(); w_label.set_accessible_role(gtk::AccessibleRole::Presentation); let width_spin = gtk::SpinButton::builder() .adjustment(>k::Adjustment::new( cfg.resize_width as f64, 0.0, 10000.0, 1.0, 100.0, 0.0, )) .numeric(true) .width_chars(6) .tooltip_text("Width") .build(); width_spin.update_property(&[ gtk::accessible::Property::Label("Width"), ]); let lock_btn = gtk::ToggleButton::builder() .icon_name("changes-prevent-symbolic") .active(true) .tooltip_text("Aspect ratio locked") .build(); lock_btn.update_property(&[ gtk::accessible::Property::Label("Lock aspect ratio"), ]); let height_spin = gtk::SpinButton::builder() .adjustment(>k::Adjustment::new( cfg.resize_height as f64, 0.0, 10000.0, 1.0, 100.0, 0.0, )) .numeric(true) .width_chars(6) .tooltip_text("Height") .build(); height_spin.update_property(&[ gtk::accessible::Property::Label("Height"), ]); let h_label = gtk::Label::builder() .label("H") .css_classes(["dim-label"]) .build(); h_label.set_accessible_role(gtk::AccessibleRole::Presentation); // Unit segmented toggle (px / %) let unit_box = gtk::Box::new(gtk::Orientation::Horizontal, 0); unit_box.add_css_class("linked"); unit_box.update_property(&[ gtk::accessible::Property::Label("Dimension unit toggle"), ]); let px_btn = gtk::Button::builder() .label("px") .tooltip_text("Use pixel dimensions (currently active)") .build(); px_btn.update_property(&[ gtk::accessible::Property::Label("Pixels - currently active"), ]); let pct_btn = gtk::Button::builder() .label("%") .tooltip_text("Use percentage dimensions") .build(); pct_btn.update_property(&[ gtk::accessible::Property::Label("Percentage"), ]); px_btn.add_css_class("suggested-action"); unit_box.append(&px_btn); unit_box.append(&pct_btn); dim_row.append(&w_label); dim_row.append(&width_spin); dim_row.append(&lock_btn); dim_row.append(&height_spin); dim_row.append(&h_label); dim_row.append(&unit_box); dims_group.add(&dim_row); // Mode let mode_row = adw::ComboRow::builder() .title("Mode") .subtitle("How dimensions are applied to images") .use_subtitle(true) .tooltip_text("Exact stretches to dimensions; Fit keeps aspect ratio") .build(); mode_row.set_model(Some(>k::StringList::new(&[ "Exact Size", "Fit Within Box", ]))); mode_row.set_list_factory(Some(&super::full_text_list_factory())); // Upscale let upscale_row = adw::SwitchRow::builder() .title("Allow Upscaling") .subtitle("Enlarge images smaller than target size") .active(cfg.allow_upscale) .tooltip_text("When off, images smaller than target are left as-is") .build(); dims_group.add(&mode_row); dims_group.add(&upscale_row); controls.append(&dims_group); // --- Group 3: Advanced --- let advanced_group = adw::PreferencesGroup::builder() .title("Advanced") .build(); let advanced_expander = adw::ExpanderRow::builder() .title("Advanced Settings") .subtitle("Resize algorithm and output DPI") .show_enable_switch(false) .expanded(state.is_section_expanded("resize-advanced")) .build(); { let st = state.clone(); advanced_expander.connect_expanded_notify(move |row| { st.set_section_expanded("resize-advanced", row.is_expanded()); }); } let algorithm_row = adw::ComboRow::builder() .title("Resize Algorithm") .use_subtitle(true) .build(); algorithm_row.set_model(Some(>k::StringList::new(&[ "Lanczos3 (Best quality)", "CatmullRom (Good, faster)", "Bilinear (Fast)", "Nearest (Pixelated)", ]))); algorithm_row.set_list_factory(Some(&super::full_text_list_factory())); let dpi_row = adw::SpinRow::builder() .title("DPI") .subtitle("Output resolution in dots per inch") .adjustment(>k::Adjustment::new(72.0, 72.0, 600.0, 1.0, 10.0, 0.0)) .build(); advanced_expander.add_row(&algorithm_row); advanced_expander.add_row(&dpi_row); advanced_group.add(&advanced_expander); controls.append(&advanced_group); controls_scroll.set_child(Some(&controls)); split.append(&preview_box); split.append(&controls_scroll); outer.append(&split); // === SHARED STATE === let preview_width = std::rc::Rc::new(std::cell::RefCell::new(cfg.resize_width)); let preview_height = std::rc::Rc::new(std::cell::RefCell::new(cfg.resize_height)); let preview_upscale = std::rc::Rc::new(std::cell::Cell::new(cfg.allow_upscale)); let preview_mode = std::rc::Rc::new(std::cell::Cell::new(cfg.resize_mode)); let preview_algo = std::rc::Rc::new(std::cell::Cell::new(0u32)); let preview_index = std::rc::Rc::new(std::cell::Cell::new(0usize)); let is_pct = std::rc::Rc::new(std::cell::Cell::new(false)); let updating = std::rc::Rc::new(std::cell::Cell::new(false)); let loaded_files = state.loaded_files.clone(); drop(cfg); // === RENDER CLOSURE === let resize_preview_gen: Rc> = Rc::new(Cell::new(0)); let render_thumb = { let lf = loaded_files.clone(); let pw = preview_width.clone(); let ph = preview_height.clone(); let pu = preview_upscale.clone(); let pm = preview_mode.clone(); let pa = preview_algo.clone(); let pi = preview_index.clone(); let pic = thumb_picture.clone(); let dlbl = dims_label.clone(); let npl = no_preview_label.clone(); let bind_gen = resize_preview_gen.clone(); std::rc::Rc::new(move || { let files = lf.borrow(); if files.is_empty() { pic.set_paintable(gtk::gdk::Paintable::NONE); pic.set_visible(false); npl.set_visible(true); npl.set_label("Add images to see resize preview"); dlbl.set_label(""); return; } let idx = pi.get() % files.len(); let Some(current) = files.get(idx) else { return }; pic.set_visible(true); npl.set_visible(false); let tw = *pw.borrow(); let th = *ph.borrow(); if tw == 0 && th == 0 { pic.set_paintable(gtk::gdk::Paintable::NONE); npl.set_visible(true); npl.set_label("Set dimensions to preview"); pic.set_visible(false); dlbl.set_label(""); return; } let file_count = files.len(); let (orig_w, orig_h) = get_image_dims(current.as_path()); let actual_tw = if tw > 0 { tw } else { orig_w }; let actual_th = if th > 0 { th } else if tw > 0 { let scale = tw as f64 / orig_w as f64; (orig_h as f64 * scale).round() as u32 } else { orig_h }; let allow_up = pu.get(); let (render_tw, render_th) = if !allow_up { (actual_tw.min(orig_w), actual_th.min(orig_h)) } else { (actual_tw, actual_th) }; let scale_pct = if orig_w > 0 { (render_tw as f64 / orig_w as f64 * 100.0).round() as u32 } else { 100 }; let counter = if file_count > 1 { format!(" [{}/{}]", idx + 1, file_count) } else { String::new() }; let clamp_note = if !allow_up && (actual_tw > orig_w || actual_th > orig_h) { " (clamped)" } else { "" }; dlbl.set_label(&format!( "{} x {} -> {} x {} ({}%){}{}", orig_w, orig_h, render_tw, render_th, scale_pct, clamp_note, counter, )); let my_gen = bind_gen.get().wrapping_add(1); bind_gen.set(my_gen); let gen_check = bind_gen.clone(); let path = current.clone(); let pic = pic.clone(); let algo = algo_index_to_filter(pa.get()); let mode = pm.get(); let (tx, rx) = std::sync::mpsc::channel::>>(); std::thread::spawn(move || { let result = (|| -> Option> { let img = image::open(&path).ok()?; let target_w = if render_tw > 0 { render_tw } else { img.width().max(1) }; let target_h = if render_th > 0 { render_th } else if img.width() > 0 { let scale = target_w as f64 / img.width() as f64; (img.height() as f64 * scale).round().max(1.0) as u32 } else { target_w }; let resized = if mode == 0 && render_th > 0 { // Exact: stretch to exact dimensions img.resize_exact(target_w.min(1024), target_h.min(1024), algo) } else { // Fit within box (or width-only): maintain aspect ratio img.resize(target_w.min(1024), target_h.min(1024), algo) }; let mut buf = Vec::new(); resized.write_to( &mut std::io::Cursor::new(&mut buf), image::ImageFormat::Png, ).ok()?; Some(buf) })(); let _ = tx.send(result); }); glib::timeout_add_local(std::time::Duration::from_millis(100), move || { if gen_check.get() != my_gen { return glib::ControlFlow::Break; } match rx.try_recv() { Ok(Some(bytes)) => { let gbytes = glib::Bytes::from(&bytes); if let Ok(texture) = gtk::gdk::Texture::from_bytes(&gbytes) { pic.set_paintable(Some(&texture)); } glib::ControlFlow::Break } Ok(None) => glib::ControlFlow::Break, Err(std::sync::mpsc::TryRecvError::Empty) => glib::ControlFlow::Continue, Err(_) => glib::ControlFlow::Break, } }); }) }; // === WIRE SIGNALS === // Enable toggle { let jc = state.job_config.clone(); enable_row.connect_active_notify(move |row| { jc.borrow_mut().resize_enabled = row.is_active(); }); } // Category selection - rebuild the size dropdown { let sr = size_row.clone(); category_row.connect_selected_notify(move |row| { rebuild_size_model(&sr, row.selected()); }); } // Size selection - auto-fill dimensions and lock aspect ratio { let ws = width_spin.clone(); let hs = height_spin.clone(); let lb = lock_btn.clone(); let jc = state.job_config.clone(); let pw = preview_width.clone(); let ph = preview_height.clone(); let upd = updating.clone(); let ip = is_pct.clone(); let px = px_btn.clone(); let pct = pct_btn.clone(); let rt = render_thumb.clone(); let cr = category_row.clone(); size_row.connect_selected_notify(move |row| { let sel = row.selected() as usize; if sel == 0 { return; // "(select a size)" - don't change anything } let presets = presets_for_category(cr.selected()); let Some(&(_, w, h)) = presets.get(sel - 1) else { return }; // Switch to pixels if currently in percentage if ip.get() { ip.set(false); px.add_css_class("suggested-action"); pct.remove_css_class("suggested-action"); ws.set_range(0.0, 10000.0); ws.set_increments(1.0, 100.0); hs.set_range(0.0, 10000.0); hs.set_increments(1.0, 100.0); } upd.set(true); ws.set_value(w as f64); hs.set_value(h as f64); upd.set(false); // Lock aspect ratio if !lb.is_active() { lb.set_active(true); } let mut c = jc.borrow_mut(); c.resize_width = w; c.resize_height = h; *pw.borrow_mut() = w; *ph.borrow_mut() = h; rt(); }); } // Lock button icon change { let lb = lock_btn.clone(); lock_btn.connect_active_notify(move |btn| { if btn.is_active() { lb.set_icon_name("changes-prevent-symbolic"); lb.set_tooltip_text(Some("Aspect ratio locked")); lb.update_property(&[ gtk::accessible::Property::Label("Aspect ratio locked - click to unlock"), ]); } else { lb.set_icon_name("changes-allow-symbolic"); lb.set_tooltip_text(Some("Aspect ratio unlocked")); lb.update_property(&[ gtk::accessible::Property::Label("Aspect ratio unlocked - click to lock"), ]); } }); } // Width spin with aspect lock { let jc = state.job_config.clone(); let pw = preview_width.clone(); let ph = preview_height.clone(); let rt = render_thumb.clone(); let lb = lock_btn.clone(); let hs = height_spin.clone(); let files = state.loaded_files.clone(); let upd = updating.clone(); let ip = is_pct.clone(); let pr = size_row.clone(); width_spin.connect_value_notify(move |spin| { if upd.get() { return; } let val = spin.value(); let pixel_val = if ip.get() { let dims = get_first_image_dims(&files.borrow()); (val / 100.0 * dims.0 as f64).round() as u32 } else { val as u32 }; jc.borrow_mut().resize_width = pixel_val; *pw.borrow_mut() = pixel_val; // Reset preset to Custom when manually editing if pr.selected() != 0 { upd.set(true); pr.set_selected(0); upd.set(false); } if lb.is_active() && val > 0.0 { if ip.get() { upd.set(true); hs.set_value(val); let dims = get_first_image_dims(&files.borrow()); let pixel_h = (val / 100.0 * dims.1 as f64).round() as u32; jc.borrow_mut().resize_height = pixel_h; *ph.borrow_mut() = pixel_h; upd.set(false); } else { let aspect = get_first_image_aspect(&files.borrow()); if aspect > 0.0 { let new_h = (val / aspect).round() as u32; upd.set(true); hs.set_value(new_h as f64); jc.borrow_mut().resize_height = new_h; *ph.borrow_mut() = new_h; upd.set(false); } } } rt(); }); } // Height spin with aspect lock { let jc = state.job_config.clone(); let pw = preview_width.clone(); let ph = preview_height.clone(); let rt = render_thumb.clone(); let lb = lock_btn.clone(); let ws = width_spin.clone(); let files = state.loaded_files.clone(); let upd = updating.clone(); let ip = is_pct.clone(); let pr = size_row.clone(); height_spin.connect_value_notify(move |spin| { if upd.get() { return; } let val = spin.value(); let pixel_val = if ip.get() { let dims = get_first_image_dims(&files.borrow()); (val / 100.0 * dims.1 as f64).round() as u32 } else { val as u32 }; jc.borrow_mut().resize_height = pixel_val; *ph.borrow_mut() = pixel_val; // Reset preset to Custom when manually editing if pr.selected() != 0 { upd.set(true); pr.set_selected(0); upd.set(false); } if lb.is_active() && val > 0.0 { if ip.get() { upd.set(true); ws.set_value(val); let dims = get_first_image_dims(&files.borrow()); let pixel_w = (val / 100.0 * dims.0 as f64).round() as u32; jc.borrow_mut().resize_width = pixel_w; *pw.borrow_mut() = pixel_w; upd.set(false); } else { let aspect = get_first_image_aspect(&files.borrow()); if aspect > 0.0 { let new_w = (val * aspect).round() as u32; upd.set(true); ws.set_value(new_w as f64); jc.borrow_mut().resize_width = new_w; *pw.borrow_mut() = new_w; upd.set(false); } } } rt(); }); } // Unit toggle: px button { let pct = pct_btn.clone(); let px = px_btn.clone(); let ip = is_pct.clone(); let ws = width_spin.clone(); let hs = height_spin.clone(); let files = state.loaded_files.clone(); let upd = updating.clone(); let jc = state.job_config.clone(); let pw = preview_width.clone(); let ph = preview_height.clone(); let rt = render_thumb.clone(); px_btn.connect_clicked(move |_| { if !ip.get() { return; } // already pixels ip.set(false); px.add_css_class("suggested-action"); pct.remove_css_class("suggested-action"); px.update_property(&[ gtk::accessible::Property::Label("Pixels - currently active"), ]); pct.update_property(&[ gtk::accessible::Property::Label("Percentage"), ]); px.set_tooltip_text(Some("Use pixel dimensions (currently active)")); pct.set_tooltip_text(Some("Use percentage dimensions")); let dims = get_first_image_dims(&files.borrow()); let pct_w = ws.value(); let pct_h = hs.value(); let pixel_w = (pct_w / 100.0 * dims.0 as f64).round(); let pixel_h = (pct_h / 100.0 * dims.1 as f64).round(); upd.set(true); ws.set_range(0.0, 10000.0); ws.set_increments(1.0, 100.0); hs.set_range(0.0, 10000.0); hs.set_increments(1.0, 100.0); ws.set_value(pixel_w); hs.set_value(pixel_h); upd.set(false); let mut c = jc.borrow_mut(); c.resize_width = pixel_w as u32; c.resize_height = pixel_h as u32; *pw.borrow_mut() = pixel_w as u32; *ph.borrow_mut() = pixel_h as u32; rt(); }); } // Unit toggle: % button { let px = px_btn.clone(); let pct = pct_btn.clone(); let ip = is_pct.clone(); let ws = width_spin.clone(); let hs = height_spin.clone(); let files = state.loaded_files.clone(); let upd = updating.clone(); let rt = render_thumb.clone(); let jc = state.job_config.clone(); let pw = preview_width.clone(); let ph = preview_height.clone(); pct_btn.connect_clicked(move |_| { if ip.get() { return; } // already percentage ip.set(true); pct.add_css_class("suggested-action"); px.remove_css_class("suggested-action"); pct.update_property(&[ gtk::accessible::Property::Label("Percentage - currently active"), ]); px.update_property(&[ gtk::accessible::Property::Label("Pixels"), ]); pct.set_tooltip_text(Some("Use percentage dimensions (currently active)")); px.set_tooltip_text(Some("Use pixel dimensions")); let dims = get_first_image_dims(&files.borrow()); let cur_w = ws.value(); let cur_h = hs.value(); let pct_w = if dims.0 > 0 { (cur_w / dims.0 as f64 * 100.0).round() } else { 100.0 }; let pct_h = if dims.1 > 0 { (cur_h / dims.1 as f64 * 100.0).round() } else { 100.0 }; upd.set(true); ws.set_range(0.0, 1000.0); ws.set_increments(1.0, 10.0); hs.set_range(0.0, 1000.0); hs.set_increments(1.0, 10.0); ws.set_value(pct_w); hs.set_value(pct_h); upd.set(false); // Update job_config with the pixel values let pixel_w = (pct_w / 100.0 * dims.0 as f64).round() as u32; let pixel_h = (pct_h / 100.0 * dims.1 as f64).round() as u32; let mut c = jc.borrow_mut(); c.resize_width = pixel_w; c.resize_height = pixel_h; *pw.borrow_mut() = pixel_w; *ph.borrow_mut() = pixel_h; rt(); }); } // Mode toggle - update labels and job_config { let ws = width_spin.clone(); let hs = height_spin.clone(); let jc = state.job_config.clone(); let rt = render_thumb.clone(); let pmode = preview_mode.clone(); mode_row.connect_selected_notify(move |row| { jc.borrow_mut().resize_mode = row.selected(); pmode.set(row.selected()); if row.selected() == 1 { ws.set_tooltip_text(Some("Maximum width")); hs.set_tooltip_text(Some("Maximum height")); } else { ws.set_tooltip_text(Some("Width")); hs.set_tooltip_text(Some("Height")); } rt(); }); } // Upscale toggle { let jc = state.job_config.clone(); let pu = preview_upscale.clone(); let rt = render_thumb.clone(); upscale_row.connect_active_notify(move |row| { jc.borrow_mut().allow_upscale = row.is_active(); pu.set(row.is_active()); rt(); }); } // Algorithm { let jc = state.job_config.clone(); let pa = preview_algo.clone(); let rt = render_thumb.clone(); algorithm_row.connect_selected_notify(move |row| { jc.borrow_mut().resize_algorithm = row.selected(); pa.set(row.selected()); rt(); }); } // DPI { let jc = state.job_config.clone(); dpi_row.connect_value_notify(move |row| { jc.borrow_mut().output_dpi = row.value() as u32; }); } // Click preview to cycle images { let pi = preview_index.clone(); let rt = render_thumb.clone(); let lf = loaded_files.clone(); let click = gtk::GestureClick::new(); click.connect_released(move |gesture, _, _, _| { let count = lf.borrow().len(); if count > 1 { pi.set((pi.get() + 1) % count); rt(); } gesture.set_state(gtk::EventSequenceState::Claimed); }); thumb_picture.set_can_target(true); thumb_picture.set_focusable(true); thumb_picture.add_controller(click); thumb_picture.set_cursor_from_name(Some("pointer")); } // Keyboard support for preview cycling (Space/Enter) { let pi = preview_index.clone(); let rt = render_thumb.clone(); let lf = loaded_files.clone(); let key = gtk::EventControllerKey::new(); key.connect_key_pressed(move |_, keyval, _, _| { if keyval == gtk::gdk::Key::space || keyval == gtk::gdk::Key::Return { let count = lf.borrow().len(); if count > 1 { pi.set((pi.get() + 1) % count); rt(); } return glib::Propagation::Stop; } glib::Propagation::Proceed }); thumb_picture.add_controller(key); } // Initial render { let rt = render_thumb.clone(); glib::idle_add_local_once(move || rt()); } let page = adw::NavigationPage::builder() .title("Resize") .tag("step-resize") .child(&outer) .build(); // Sync enable toggle and re-render on page map { let rt = render_thumb.clone(); let jc = state.job_config.clone(); let er = enable_row.clone(); page.connect_map(move |_| { let enabled = jc.borrow().resize_enabled; er.set_active(enabled); rt(); }); } page }