blob: a26f857ac748169029d4012fc31f0fadda0491a8 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
//! User-selected immutable files; paths and mutable buffers never enter guest state.
mod selection;
use crate::{ComponentCall, ComponentOutput, Emission, EventOrigin, Frame};
use maka_plugin_core::contract::{
Capture, CaptureOutput, Contract, Interface, Transport, binary_resource_type,
};
use schemars::JsonSchema;
pub use selection::{FileSelection, FileSnapshot, FileSnapshotWriter, read_selected};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use std::collections::BTreeMap;
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct FilePickerProps {
pub appearance: crate::appearance::InputAppearance,
#[schemars(length(min = 1))]
pub label: String,
#[schemars(length(min = 1))]
pub path_label: String,
#[schemars(length(min = 1))]
pub browse_label: String,
#[schemars(length(min = 1))]
pub parent_label: String,
#[schemars(length(min = 1))]
pub empty_label: String,
#[schemars(length(min = 1))]
pub truncated_label: String,
pub placeholder: String,
pub disabled: bool,
#[schemars(range(min = -1, max = 32767))]
pub tab_index: i16,
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct FileDescriptor {
pub name: String,
pub bytes: usize,
#[schemars(length(min = 71, max = 71))]
pub digest: String,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum FilePhase {
Empty,
Choosing,
Ready,
Failed,
}
#[derive(
Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize, JsonSchema, thiserror::Error,
)]
#[serde(rename_all = "snake_case")]
pub enum FileError {
#[error("selected path is not an absolute regular file")]
InvalidFile,
#[error("selected file could not be read")]
ReadFailed,
#[error("selected file exceeds the byte limit")]
LimitExceeded,
#[error("native resource budget exhausted")]
BudgetExhausted,
#[error("native file worker failed")]
WorkerFailed,
#[error("native clipboard has no supported content or is unavailable")]
ClipboardUnavailable,
#[error("clipboard text cannot be inserted into the original draft")]
PasteRejected,
}
impl From<crate::budget::BudgetExhausted> for FileError {
fn from(_: crate::budget::BudgetExhausted) -> Self {
Self::BudgetExhausted
}
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct FileObservation {
#[schemars(length(min = 1))]
pub owner: String,
pub revision: u64,
pub phase: FilePhase,
pub file: Option<FileDescriptor>,
pub error: Option<FileError>,
}
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct FileCapture {
#[schemars(length(min = 1))]
pub owner: String,
pub revision: u64,
}
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct FileChoose {
#[schemars(length(min = 1))]
pub request: String,
}
/// Committed mechanical intent; only a renderer's matching private gesture can act.
#[derive(Clone)]
pub struct FileView {
pub props: FilePickerProps,
pub request: Option<String>,
}
pub fn observation_schema() -> Value {
serde_json::to_value(schemars::schema_for!(FileObservation)).expect("file observation")
}
pub fn choose_schema() -> Value {
serde_json::to_value(schemars::schema_for!(FileChoose)).expect("file choose")
}
pub fn file_picker_contract() -> Contract {
Contract {
name: "maka.ui.FilePicker".into(),
version: 1,
transport: Transport::Local,
interface: Interface::Component {
props: serde_json::to_value(schemars::schema_for!(FilePickerProps))
.expect("file props")
.into(),
events: BTreeMap::from([("changed".into(), observation_schema().into())]),
captures: BTreeMap::from([(
"snapshot".into(),
Capture {
input: serde_json::to_value(schemars::schema_for!(FileCapture))
.expect("file capture")
.into(),
output: CaptureOutput::Resource {
resource: binary_resource_type(),
},
},
)]),
},
}
}
pub fn reduce(call: ComponentCall<'_>) -> crate::Result<ComponentOutput<FileView>> {
let (props, revision, request, observation, events) = match call {
ComponentCall::Mount { props } => (props, 0, Value::Null, Value::Null, vec![]),
ComponentCall::Update { state, props } => (
props,
state["revision"].as_u64().expect("file UI revision") + 1,
state["request"].clone(),
state["observation"].clone(),
vec![],
),
ComponentCall::Event {
state,
props,
key,
input,
origin,
} => {
if !matches!(origin, EventOrigin::Raw) {
return Err(crate::Error::Invalid(
"file input requires native origin".into(),
));
}
let revision = state["revision"].as_u64().expect("file UI revision") + 1;
match key {
"choose" => {
let typed: FilePickerProps = serde_json::from_value(props.clone())
.map_err(|e| crate::Error::Invalid(e.to_string()))?;
if typed.disabled {
return Err(crate::Error::Invalid("file picker disabled".into()));
}
let choose: FileChoose = serde_json::from_value(input.clone())
.map_err(|e| crate::Error::Invalid(e.to_string()))?;
(
props,
revision,
json!(choose.request),
state["observation"].clone(),
vec![],
)
}
"observe" => (
props,
revision,
state["request"].clone(),
input.clone(),
vec![Emission {
name: "changed".into(),
value: input.clone(),
}],
),
_ => return Err(crate::Error::Invalid("unknown file input".into())),
}
}
};
let typed =
serde_json::from_value(props.clone()).map_err(|e| crate::Error::Invalid(e.to_string()))?;
let request_value = serde_json::from_value(request.clone()).expect("file request marker");
Ok(Frame {
revision,
state: json!({"revision": revision, "request":request, "observation": observation}),
surface: None,
view: FileView {
props: typed,
request: request_value,
},
}
.output(events))
}
pub const IMPLEMENTATION: &str = "maka.ui.file";
pub fn native() -> maka_plugin_core::package::NativeDefinition {
maka_plugin_core::package::NativeDefinition::new(
IMPLEMENTATION,
serde_json::json!({"type":"object","properties":{},"required":[],"additionalProperties":false}),
[("FilePicker", file_picker_contract())],
)
}