blob: e945eef90f5b0e215bd1ac6e085de1e1e8dc48ef [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.
*/
//! Native reading evidence and commands. Layout remains the selected scene language.
use maka_plugin_core::contract::{Capture, CaptureOutput, Contract, Interface, Transport};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeMap;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ReadingProps {
/// The presenter's scene language; each requires a scrolling column Stack.
/// Its scroll policy and layout are the only presentation authority.
pub content: Value,
/// Omission retains an accepted command; numbers increase per original mount.
pub command: Option<ReadingCommand>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ReadingCommand {
#[schemars(range(min = 1))]
pub sequence: u64,
pub target: ReadingTarget,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub enum ReadingTarget {
Start,
End,
/// Find over native readable descendants, including folded slots. SourceText uses
/// original literal bytes; Markdown uses its width-independent rendered text.
Find {
query: String,
step: FindStep,
},
ClearFind,
/// Move the original terminal viewport by physical rows.
ScrollRows {
rows: i32,
},
/// Reveal a UTF-8 byte in the descendant's native readable text, as returned by Find.
RevealSource {
#[schemars(length(min = 1))]
path: Vec<String>,
offset: u32,
/// If supplied, reveal only while this accepted find retains its original physical intent.
#[serde(default)]
find_sequence: Option<u64>,
},
/// TUI: focus and minimally reveal an enabled scene Item. Settlement waits
/// for its successful presentation; this never submits the item.
FocusItem {
#[schemars(length(min = 1))]
key: String,
},
RevealChild {
#[schemars(length(min = 1))]
key: String,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum FindStep {
First,
Next,
Previous,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ReadingMatch {
#[schemars(length(min = 1))]
pub path: Vec<String>,
pub offset: u32,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ReadingFind {
pub sequence: u64,
pub count: u32,
pub limited: bool,
/// One-based current match, or null when no match remains.
pub index: Option<u32>,
pub active: Option<ReadingMatch>,
}
impl ReadingCommand {
pub fn valid_target(&self) -> bool {
match &self.target {
ReadingTarget::Find { .. } => true,
ReadingTarget::FocusItem { key } => !key.is_empty(),
ReadingTarget::ScrollRows { .. } => true,
ReadingTarget::RevealSource {
path,
offset: _,
find_sequence,
} => {
find_sequence.is_none_or(|sequence| sequence > 0)
&& !path.is_empty()
&& path.iter().all(|key| !key.is_empty())
}
_ => true,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ReadingStatus {
Applied,
Cancelled,
Rejected,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ReadingReceipt {
pub sequence: u64,
pub status: ReadingStatus,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ReadingViewport {
/// Explicit reading intent used by the actually painted children.
pub reading_revision: u64,
#[serde(default)]
pub at_start: bool,
pub at_end: bool,
pub visible_child_keys: Vec<String>,
}
/// Most recent physical scroll. Layout changes and programmatic landings do not
/// create a new request, so a product can load adjacent content without loops.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ReadingScroll {
pub reading_revision: u64,
pub direction: ReadingScrollDirection,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ReadingScrollDirection {
Up,
Down,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ReadingObservation {
/// Immediate native intent; can be newer than the painted viewport.
pub reading_revision: u64,
pub viewport: Option<ReadingViewport>,
#[serde(default)]
pub scroll: Option<ReadingScroll>,
/// Direct content child containing the native selection focus. Renderers
/// without this observation return null. This key conveys no read authority.
pub selected_child_key: Option<String>,
pub pending_command: Option<u64>,
/// Only the most recently settled command is retained, without a ledger.
pub receipt: Option<ReadingReceipt>,
pub find: Option<ReadingFind>,
}
pub fn observation_schema() -> Value {
serde_json::to_value(schemars::schema_for!(ReadingObservation)).expect("reading observation")
}
pub fn reading_contract() -> Contract {
Contract {
name: "maka.ui.ReadingArea".into(),
version: 1,
transport: Transport::Local,
interface: Interface::Component {
props: serde_json::to_value(schemars::schema_for!(ReadingProps))
.expect("reading props")
.into(),
events: BTreeMap::from([
("observed".into(), observation_schema().into()),
(
"input".into(),
serde_json::to_value(schemars::schema_for!(crate::abi::ComponentInput))
.expect("reading input")
.into(),
),
]),
captures: BTreeMap::from([(
"tail".into(),
Capture {
input: serde_json::json!({"type":"null"}).into(),
output: CaptureOutput::Continuation,
},
)]),
},
}
}
pub const IMPLEMENTATION: &str = "maka.ui.reading";
pub fn native() -> maka_plugin_core::package::NativeDefinition {
maka_plugin_core::package::NativeDefinition::new(
IMPLEMENTATION,
serde_json::json!({"type":"object","properties":{},"required":[],"additionalProperties":false}),
[("ReadingArea", reading_contract())],
)
}