use anyhow::{Context, anyhow}; use db::kvp::KeyValueStore; use gpui::App; use ui::Window; use crate::preview; use crate::shape::{self, Centered, RelativeHeight, RelativeWidth, Shape, ViewportFraction}; const PICKERS_NAMESPACE: &str = "pickers_v2"; pub(crate) fn store_shape_for_this_layout( picker_delegate: &'static str, preview_layout: Option, shape: shape::Centered, window: &ui::Window, cx: &App, ) { let shape = PickerConfig::from_centered(shape, window); let kvp = KeyValueStore::global(cx); db::write_and_log(cx, async move || { kvp.scoped(PICKERS_NAMESPACE) .write( shape_key(picker_delegate, preview_layout), serde_json::to_string(&shape).context("Could not serialize size of picker")?, ) .await?; // Must be written after the shape or loading can break // if this future is ever cancelled. kvp.scoped(PICKERS_NAMESPACE) .write( last_layout_key(picker_delegate), layout_as_str(preview_layout).to_string(), ) .await }); } /// Persist the last-used preview layout without touching the stored shape. /// /// Changing the preview position (e.g. right/below/hidden) without resizing /// must still be remembered, so this is called whenever the layout changes. pub(crate) fn store_last_layout( picker_delegate: &'static str, preview_layout: Option, cx: &App, ) { let kvp = KeyValueStore::global(cx); db::write_and_log(cx, async move || { kvp.scoped(PICKERS_NAMESPACE) .write( last_layout_key(picker_delegate), layout_as_str(preview_layout).to_string(), ) .await }); } pub(crate) fn try_load_shape( picker_delegate: &'static str, preview_layout: impl Into>, cx: &App, ) -> anyhow::Result> { let Some(shape) = KeyValueStore::global(cx) .scoped(PICKERS_NAMESPACE) .read(&shape_key(picker_delegate, preview_layout.into())) .context("Could not read picker shape from KeyValueStore")? else { return Ok(None); }; let shape = serde_json::from_str::(&shape) .context("Could not deserialize loaded picker shape from persistence")? .into_centered(); Ok(Some(Shape::HorizontallyCentered(shape))) } pub(crate) fn load_last_preview_layout( picker_delegate: &'static str, cx: &App, ) -> anyhow::Result> { let Some(last_layout) = KeyValueStore::global(cx) .scoped(PICKERS_NAMESPACE) .read(&last_layout_key(picker_delegate)) .context("Could not read last picker layout from KeyValueStore")? else { return Ok(None); }; parse_layout(&last_layout) } fn shape_key(picker_delegate: &'static str, preview_layout: Option) -> String { format!("{picker_delegate}/{}", layout_as_str(preview_layout)) } fn last_layout_key(picker_delegate: &'static str) -> String { format!("{picker_delegate}/LAST_PREVIEW_LAYOUT") } fn layout_as_str(layout: Option) -> &'static str { match layout { Some(preview::Layout::Hidden) => "hidden", Some(preview::Layout::Below) => "below", Some(preview::Layout::Right) => "right", None => "none", } } fn parse_layout(s: &str) -> anyhow::Result> { Ok(Some(match s { "hidden" => preview::Layout::Hidden, "below" => preview::Layout::Below, "right" => preview::Layout::Right, "none" => return Ok(None), _ => return Err(anyhow!("Unknown layout: `{}`", s)), })) } /// A resized picker for persisting its size. All values are stored as fractions /// of the viewport so they remain meaningful across window sizes. #[derive(Clone, Copy, serde::Serialize, serde::Deserialize)] pub(crate) struct PickerConfig { width: f32, // relative fraction of viewport height: f32, // relative fraction of viewport preview_size: f32, // relative fraction of viewport } impl PickerConfig { pub(crate) fn from_centered(shape: Centered, window: &Window) -> Self { PickerConfig { width: shape.width.as_viewport_fraction(window).raw(), height: shape.height.as_viewport_fraction(window).raw(), preview_size: shape.preview_size.raw(), } } pub(crate) fn into_centered(self) -> Centered { Centered { width: RelativeWidth::viewport(self.width.clamp(0.0, 1.0)), height: RelativeHeight::viewport(self.height.clamp(0.0, 1.0)), preview_size: ViewportFraction::fraction(self.preview_size.clamp(0.0, 1.0)), } } }