• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (c) 2021-2023 Huawei Device Co., Ltd.
3  * Licensed under the Apache License, Version 2.0 (the "License");
4  * you may not use this file except in compliance with the License.
5  * You may obtain a copy of the License at
6  *
7  *     http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14  */
15 
16 #include "bridge/declarative_frontend/jsview/js_scroll.h"
17 
18 #include "base/utils/utils.h"
19 #include "bridge/declarative_frontend/jsview/js_scrollable.h"
20 #include "bridge/declarative_frontend/jsview/js_scroller.h"
21 #include "bridge/declarative_frontend/jsview/js_view_common_def.h"
22 #include "bridge/declarative_frontend/jsview/models/scroll_model_impl.h"
23 #include "core/common/container.h"
24 #include "core/components/common/layout/constants.h"
25 #include "core/components/scroll/scrollable.h"
26 #include "core/components_ng/pattern/scroll/inner/scroll_bar.h"
27 #include "core/components_ng/pattern/scroll/scroll_model.h"
28 #include "core/components_ng/pattern/scroll/scroll_model_ng.h"
29 
30 namespace OHOS::Ace {
31 
32 std::unique_ptr<ScrollModel> ScrollModel::instance_ = nullptr;
33 std::mutex ScrollModel::mutex_;
34 
GetInstance()35 ScrollModel* ScrollModel::GetInstance()
36 {
37     if (!instance_) {
38         std::lock_guard<std::mutex> lock(mutex_);
39         if (!instance_) {
40 #ifdef NG_BUILD
41             instance_.reset(new NG::ScrollModelNG());
42 #else
43             if (Container::IsCurrentUseNewPipeline()) {
44                 instance_.reset(new NG::ScrollModelNG());
45             } else {
46                 instance_.reset(new Framework::ScrollModelImpl());
47             }
48 #endif
49         }
50     }
51     return instance_.get();
52 }
53 
54 } // namespace OHOS::Ace
55 
56 namespace OHOS::Ace::Framework {
57 namespace {
58 const std::vector<Axis> AXIS = { Axis::VERTICAL, Axis::HORIZONTAL, Axis::FREE, Axis::NONE };
59 
ParseJsDimensionArray(const JSRef<JSVal> & jsValue,std::vector<Dimension> & result)60 bool ParseJsDimensionArray(const JSRef<JSVal>& jsValue, std::vector<Dimension>& result)
61 {
62     if (!jsValue->IsArray()) {
63         return false;
64     }
65     JSRef<JSArray> array = JSRef<JSArray>::Cast(jsValue);
66     for (size_t i = 0; i < array->Length(); i++) {
67         JSRef<JSVal> value = array->GetValueAt(i);
68         CalcDimension dimension;
69         if (JSViewAbstract::ParseJsDimensionVp(value, dimension)) {
70             result.emplace_back(static_cast<Dimension>(dimension));
71         } else {
72             return false;
73         }
74     }
75     return true;
76 }
77 
CheckSnapPaginations(std::vector<Dimension> snapPaginations)78 bool CheckSnapPaginations(std::vector<Dimension> snapPaginations)
79 {
80     CHECK_NULL_RETURN(!snapPaginations.empty(), false);
81     float preValue = (*snapPaginations.begin()).Value();
82     CHECK_NULL_RETURN(!Negative(preValue), false);
83     auto unit = (*snapPaginations.begin()).Unit();
84     for (auto iter = snapPaginations.begin() + 1; iter < snapPaginations.end(); ++iter) {
85         if (Negative((*iter).Value()) || (*iter).Unit() != unit || LessOrEqual((*iter).Value(), preValue)) {
86             return false;
87         }
88         preValue = (*iter).Value();
89     }
90     return true;
91 }
92 } // namespace
93 
Create(const JSCallbackInfo & info)94 void JSScroll::Create(const JSCallbackInfo& info)
95 {
96     ScrollModel::GetInstance()->Create();
97     if (info.Length() > 0 && info[0]->IsObject()) {
98         JSScroller* jsScroller = JSRef<JSObject>::Cast(info[0])->Unwrap<JSScroller>();
99         if (jsScroller) {
100             jsScroller->SetInstanceId(Container::CurrentId());
101             auto positionController = ScrollModel::GetInstance()->GetOrCreateController();
102             jsScroller->SetController(positionController);
103             // Init scroll bar proxy.
104             auto proxy = jsScroller->GetScrollBarProxy();
105             if (!proxy) {
106                 proxy = ScrollModel::GetInstance()->CreateScrollBarProxy();
107                 jsScroller->SetScrollBarProxy(proxy);
108             }
109             ScrollModel::GetInstance()->SetScrollBarProxy(proxy);
110         }
111     }
112     // init scroll bar
113     std::pair<bool, Color> barColor;
114     barColor.first = false;
115     std::pair<bool, Dimension> barWidth;
116     barWidth.first = false;
117     ScrollModel::GetInstance()->InitScrollBar(GetTheme<ScrollBarTheme>(), barColor, barWidth, EdgeEffect::NONE);
118 }
119 
SetScrollable(int32_t value)120 void JSScroll::SetScrollable(int32_t value)
121 {
122     if (value < 0 || value >= static_cast<int32_t>(AXIS.size())) {
123         return;
124     }
125     ScrollModel::GetInstance()->SetAxis(AXIS[value]);
126 }
127 
SetScrollEnabled(const JSCallbackInfo & args)128 void JSScroll::SetScrollEnabled(const JSCallbackInfo& args)
129 {
130     ScrollModel::GetInstance()->SetScrollEnabled(args[0]->IsBoolean() ? args[0]->ToBoolean() : true);
131 }
132 
OnScrollBeginCallback(const JSCallbackInfo & args)133 void JSScroll::OnScrollBeginCallback(const JSCallbackInfo& args)
134 {
135     if (args[0]->IsFunction()) {
136         auto onScrollBegin = [execCtx = args.GetExecutionContext(), func = JSRef<JSFunc>::Cast(args[0])](
137                                  const Dimension& dx, const Dimension& dy) -> ScrollInfo {
138             ScrollInfo scrollInfo { .dx = dx, .dy = dy };
139             JAVASCRIPT_EXECUTION_SCOPE_WITH_CHECK(execCtx, scrollInfo);
140             auto params = ConvertToJSValues(dx, dy);
141             auto result = func->Call(JSRef<JSObject>(), params.size(), params.data());
142             if (result.IsEmpty()) {
143                 return scrollInfo;
144             }
145 
146             if (!result->IsObject()) {
147                 return scrollInfo;
148             }
149 
150             auto resObj = JSRef<JSObject>::Cast(result);
151             auto dxRemainValue = resObj->GetProperty("dxRemain");
152             if (dxRemainValue->IsNumber()) {
153                 scrollInfo.dx = Dimension(dxRemainValue->ToNumber<float>(), DimensionUnit::VP);
154             }
155             auto dyRemainValue = resObj->GetProperty("dyRemain");
156             if (dyRemainValue->IsNumber()) {
157                 scrollInfo.dy = Dimension(dyRemainValue->ToNumber<float>(), DimensionUnit::VP);
158             }
159             return scrollInfo;
160         };
161         ScrollModel::GetInstance()->SetOnScrollBegin(std::move(onScrollBegin));
162     }
163     args.SetReturnValue(args.This());
164 }
165 
OnScrollFrameBeginCallback(const JSCallbackInfo & args)166 void JSScroll::OnScrollFrameBeginCallback(const JSCallbackInfo& args)
167 {
168     if (args[0]->IsFunction()) {
169         auto onScrollFrameBegin = [execCtx = args.GetExecutionContext(), func = JSRef<JSFunc>::Cast(args[0])](
170                                       const Dimension& offset, ScrollState state) -> ScrollFrameResult {
171             OHOS::Ace::ScrollFrameResult scrollRes { .offset = offset };
172             JAVASCRIPT_EXECUTION_SCOPE_WITH_CHECK(execCtx, scrollRes);
173             auto params = ConvertToJSValues(offset, state);
174             auto result = func->Call(JSRef<JSObject>(), params.size(), params.data());
175             if (result.IsEmpty()) {
176                 return scrollRes;
177             }
178 
179             if (!result->IsObject()) {
180                 return scrollRes;
181             }
182 
183             auto resObj = JSRef<JSObject>::Cast(result);
184             auto dxRemainValue = resObj->GetProperty("offsetRemain");
185             if (dxRemainValue->IsNumber()) {
186                 scrollRes.offset = Dimension(dxRemainValue->ToNumber<float>(), DimensionUnit::VP);
187             }
188             return scrollRes;
189         };
190         ScrollModel::GetInstance()->SetOnScrollFrameBegin(std::move(onScrollFrameBegin));
191     }
192     args.SetReturnValue(args.This());
193 }
194 
OnScrollCallback(const JSCallbackInfo & args)195 void JSScroll::OnScrollCallback(const JSCallbackInfo& args)
196 {
197     if (args[0]->IsFunction()) {
198         auto onScroll = [execCtx = args.GetExecutionContext(), func = JSRef<JSFunc>::Cast(args[0])](
199                             const Dimension& xOffset, const Dimension& yOffset) {
200             JAVASCRIPT_EXECUTION_SCOPE_WITH_CHECK(execCtx);
201             auto params = ConvertToJSValues(xOffset, yOffset);
202             func->Call(JSRef<JSObject>(), params.size(), params.data());
203         };
204         ScrollModel::GetInstance()->SetOnScroll(std::move(onScroll));
205     }
206     args.SetReturnValue(args.This());
207 }
208 
OnScrollEdgeCallback(const JSCallbackInfo & args)209 void JSScroll::OnScrollEdgeCallback(const JSCallbackInfo& args)
210 {
211     if (args[0]->IsFunction()) {
212         auto scrollEdge = [execCtx = args.GetExecutionContext(), func = JSRef<JSFunc>::Cast(args[0])](
213                               const NG::ScrollEdge& side) {
214             JAVASCRIPT_EXECUTION_SCOPE_WITH_CHECK(execCtx);
215             auto params = ConvertToJSValues(side);
216             func->Call(JSRef<JSObject>(), 1, params.data());
217         };
218         ScrollModel::GetInstance()->SetOnScrollEdge(std::move(scrollEdge));
219     }
220     args.SetReturnValue(args.This());
221 }
222 
OnScrollEndCallback(const JSCallbackInfo & args)223 void JSScroll::OnScrollEndCallback(const JSCallbackInfo& args)
224 {
225     if (args[0]->IsFunction()) {
226         auto scrollEnd = [execCtx = args.GetExecutionContext(), func = JSRef<JSFunc>::Cast(args[0])]() {
227             JAVASCRIPT_EXECUTION_SCOPE_WITH_CHECK(execCtx);
228             func->Call(JSRef<JSObject>(), 0, nullptr);
229         };
230         ScrollModel::GetInstance()->SetOnScrollEnd(std::move(scrollEnd));
231     }
232     args.SetReturnValue(args.This());
233 }
234 
OnScrollStartCallback(const JSCallbackInfo & args)235 void JSScroll::OnScrollStartCallback(const JSCallbackInfo& args)
236 {
237     if (args[0]->IsFunction()) {
238         auto scrollStart = [execCtx = args.GetExecutionContext(), func = JSRef<JSFunc>::Cast(args[0])]() {
239             JAVASCRIPT_EXECUTION_SCOPE_WITH_CHECK(execCtx);
240             func->Call(JSRef<JSObject>(), 0, nullptr);
241         };
242         ScrollModel::GetInstance()->SetOnScrollStart(std::move(scrollStart));
243     }
244     args.SetReturnValue(args.This());
245 }
246 
OnScrollStopCallback(const JSCallbackInfo & args)247 void JSScroll::OnScrollStopCallback(const JSCallbackInfo& args)
248 {
249     if (args[0]->IsFunction()) {
250         auto scrollStop = [execCtx = args.GetExecutionContext(), func = JSRef<JSFunc>::Cast(args[0])]() {
251             JAVASCRIPT_EXECUTION_SCOPE_WITH_CHECK(execCtx);
252             func->Call(JSRef<JSObject>(), 0, nullptr);
253         };
254         ScrollModel::GetInstance()->SetOnScrollStop(std::move(scrollStop));
255     }
256     args.SetReturnValue(args.This());
257 }
258 
ReachStartCallback(const JSCallbackInfo & args)259 void JSScroll::ReachStartCallback(const JSCallbackInfo& args)
260 {
261     if (args[0]->IsFunction()) {
262         auto onReachStart = [execCtx = args.GetExecutionContext(), func = JSRef<JSFunc>::Cast(args[0])]() {
263             func->Call(JSRef<JSObject>());
264             return;
265         };
266         ScrollModel::GetInstance()->SetOnReachStart(std::move(onReachStart));
267     }
268     args.ReturnSelf();
269 }
270 
ReachEndCallback(const JSCallbackInfo & args)271 void JSScroll::ReachEndCallback(const JSCallbackInfo& args)
272 {
273     if (args[0]->IsFunction()) {
274         auto onReachEnd = [execCtx = args.GetExecutionContext(), func = JSRef<JSFunc>::Cast(args[0])]() {
275             func->Call(JSRef<JSObject>());
276             return;
277         };
278         ScrollModel::GetInstance()->SetOnReachEnd(std::move(onReachEnd));
279     }
280     args.ReturnSelf();
281 }
282 
JSBind(BindingTarget globalObj)283 void JSScroll::JSBind(BindingTarget globalObj)
284 {
285     JSClass<JSScroll>::Declare("Scroll");
286     MethodOptions opt = MethodOptions::NONE;
287     JSClass<JSScroll>::StaticMethod("create", &JSScroll::Create, opt);
288     JSClass<JSScroll>::StaticMethod("scrollable", &JSScroll::SetScrollable, opt);
289     JSClass<JSScroll>::StaticMethod("onScrollBegin", &JSScroll::OnScrollBeginCallback, opt);
290     JSClass<JSScroll>::StaticMethod("onScrollFrameBegin", &JSScroll::OnScrollFrameBeginCallback, opt);
291     JSClass<JSScroll>::StaticMethod("onScroll", &JSScroll::OnScrollCallback, opt);
292     JSClass<JSScroll>::StaticMethod("onScrollEdge", &JSScroll::OnScrollEdgeCallback, opt);
293     JSClass<JSScroll>::StaticMethod("onScrollEnd", &JSScroll::OnScrollEndCallback, opt);
294     JSClass<JSScroll>::StaticMethod("onScrollStart", &JSScroll::OnScrollStartCallback, opt);
295     JSClass<JSScroll>::StaticMethod("onScrollStop", &JSScroll::OnScrollStopCallback, opt);
296     JSClass<JSScroll>::StaticMethod("onReachStart", &JSScroll::ReachStartCallback);
297     JSClass<JSScroll>::StaticMethod("onReachEnd", &JSScroll::ReachEndCallback);
298     JSClass<JSScroll>::StaticMethod("onClick", &JSInteractableView::JsOnClick);
299     JSClass<JSScroll>::StaticMethod("onTouch", &JSInteractableView::JsOnTouch);
300     JSClass<JSScroll>::StaticMethod("onHover", &JSInteractableView::JsOnHover);
301     JSClass<JSScroll>::StaticMethod("onKeyEvent", &JSInteractableView::JsOnKey);
302     JSClass<JSScroll>::StaticMethod("onDeleteEvent", &JSInteractableView::JsOnDelete);
303     JSClass<JSScroll>::StaticMethod("onAppear", &JSInteractableView::JsOnAppear);
304     JSClass<JSScroll>::StaticMethod("onDisAppear", &JSInteractableView::JsOnDisAppear);
305     JSClass<JSScroll>::StaticMethod("edgeEffect", &JSScroll::SetEdgeEffect, opt);
306     JSClass<JSScroll>::StaticMethod("scrollBar", &JSScroll::SetScrollBar, opt);
307     JSClass<JSScroll>::StaticMethod("scrollBarColor", &JSScroll::SetScrollBarColor, opt);
308     JSClass<JSScroll>::StaticMethod("scrollBarWidth", &JSScroll::SetScrollBarWidth, opt);
309     JSClass<JSScroll>::StaticMethod("remoteMessage", &JSInteractableView::JsCommonRemoteMessage);
310     JSClass<JSScroll>::StaticMethod("width", &JSScroll::JsWidth);
311     JSClass<JSScroll>::StaticMethod("height", &JSScroll::JsHeight);
312     JSClass<JSScroll>::StaticMethod("nestedScroll", &JSScroll::SetNestedScroll);
313     JSClass<JSScroll>::StaticMethod("enableScrollInteraction", &JSScroll::SetScrollEnabled);
314     JSClass<JSScroll>::StaticMethod("friction", &JSScroll::SetFriction);
315     JSClass<JSScroll>::StaticMethod("scrollSnap", &JSScroll::SetScrollSnap);
316     JSClass<JSScroll>::StaticMethod("clip", &JSScrollable::JsClip);
317     JSClass<JSScroll>::StaticMethod("enablePaging", &JSScroll::SetEnablePaging);
318     JSClass<JSScroll>::InheritAndBind<JSScrollableBase>(globalObj);
319 }
320 
SetScrollBar(const JSCallbackInfo & args)321 void JSScroll::SetScrollBar(const JSCallbackInfo& args)
322 {
323     if (args.Length() < 1) {
324         return;
325     }
326     int32_t displayMode;
327     if (args[0]->IsNull() || args[0]->IsUndefined() || !ParseJsInt32(args[0], displayMode)) {
328         displayMode = static_cast<int32_t>(DisplayMode::AUTO);
329     }
330     ScrollModel::GetInstance()->SetDisplayMode(displayMode);
331 }
332 
SetScrollBarWidth(const JSCallbackInfo & args)333 void JSScroll::SetScrollBarWidth(const JSCallbackInfo& args)
334 {
335     auto pipelineContext = PipelineContext::GetCurrentContext();
336     CHECK_NULL_VOID(pipelineContext);
337     auto theme = pipelineContext->GetTheme<ScrollBarTheme>();
338     CHECK_NULL_VOID(theme);
339     CalcDimension scrollBarWidth;
340     if (args.Length() < 1) {
341         return;
342     }
343     if (!ParseJsDimensionVp(args[0], scrollBarWidth) || args[0]->IsNull() || args[0]->IsUndefined() ||
344         (args[0]->IsString() && args[0]->ToString().empty()) || LessNotEqual(scrollBarWidth.Value(), 0.0) ||
345         scrollBarWidth.Unit() == DimensionUnit::PERCENT) {
346         scrollBarWidth = theme->GetNormalWidth();
347     }
348     ScrollModel::GetInstance()->SetScrollBarWidth(scrollBarWidth);
349 }
350 
SetScrollBarColor(const std::string & scrollBarColor)351 void JSScroll::SetScrollBarColor(const std::string& scrollBarColor)
352 {
353     if (scrollBarColor.empty()) {
354         return;
355     }
356     auto pipelineContext = PipelineContext::GetCurrentContext();
357     CHECK_NULL_VOID(pipelineContext);
358     auto theme = pipelineContext->GetTheme<ScrollBarTheme>();
359     CHECK_NULL_VOID(theme);
360     Color color(theme->GetForegroundColor());
361     Color::ParseColorString(scrollBarColor, color);
362     ScrollModel::GetInstance()->SetScrollBarColor(color);
363 }
364 
SetEdgeEffect(const JSCallbackInfo & args)365 void JSScroll::SetEdgeEffect(const JSCallbackInfo& args)
366 {
367     auto edgeEffect = JSScrollable::ParseEdgeEffect(args, EdgeEffect::NONE);
368     auto alwaysEnabled = JSScrollable::ParseAlwaysEnable(args, true);
369     ScrollModel::GetInstance()->SetEdgeEffect(edgeEffect, alwaysEnabled);
370 }
371 
JsWidth(const JSCallbackInfo & info)372 void JSScroll::JsWidth(const JSCallbackInfo& info)
373 {
374     JSViewAbstract::JsWidth(info);
375     ScrollModel::GetInstance()->SetHasWidth(true);
376 }
377 
JsHeight(const JSCallbackInfo & info)378 void JSScroll::JsHeight(const JSCallbackInfo& info)
379 {
380     JSViewAbstract::JsHeight(info);
381     ScrollModel::GetInstance()->SetHasHeight(true);
382 }
383 
SetNestedScroll(const JSCallbackInfo & args)384 void JSScroll::SetNestedScroll(const JSCallbackInfo& args)
385 {
386     NestedScrollOptions nestedOpt = {
387         .forward = NestedScrollMode::SELF_ONLY,
388         .backward = NestedScrollMode::SELF_ONLY,
389     };
390     if (args.Length() < 1 || !args[0]->IsObject()) {
391         ScrollModel::GetInstance()->SetNestedScroll(nestedOpt);
392         return;
393     }
394     JSRef<JSObject> obj = JSRef<JSObject>::Cast(args[0]);
395     int32_t froward = 0;
396     JSViewAbstract::ParseJsInt32(obj->GetProperty("scrollForward"), froward);
397     if (froward < static_cast<int32_t>(NestedScrollMode::SELF_ONLY) ||
398         froward > static_cast<int32_t>(NestedScrollMode::PARALLEL)) {
399         froward = 0;
400     }
401     int32_t backward = 0;
402     JSViewAbstract::ParseJsInt32(obj->GetProperty("scrollBackward"), backward);
403     if (backward < static_cast<int32_t>(NestedScrollMode::SELF_ONLY) ||
404         backward > static_cast<int32_t>(NestedScrollMode::PARALLEL)) {
405         backward = 0;
406     }
407     nestedOpt.forward = static_cast<NestedScrollMode>(froward);
408     nestedOpt.backward = static_cast<NestedScrollMode>(backward);
409     ScrollModel::GetInstance()->SetNestedScroll(nestedOpt);
410     args.ReturnSelf();
411 }
412 
SetFriction(const JSCallbackInfo & info)413 void JSScroll::SetFriction(const JSCallbackInfo& info)
414 {
415     double friction = -1.0;
416     if (!JSViewAbstract::ParseJsDouble(info[0], friction)) {
417         friction = -1.0;
418     }
419     ScrollModel::GetInstance()->SetFriction(friction);
420 }
421 
SetScrollSnap(const JSCallbackInfo & args)422 void JSScroll::SetScrollSnap(const JSCallbackInfo& args)
423 {
424     if (args.Length() < 1 || !args[0]->IsObject()) {
425         return;
426     }
427     JSRef<JSObject> obj = JSRef<JSObject>::Cast(args[0]);
428     auto snapAlignValue = obj->GetProperty("snapAlign");
429     int32_t snapAlign = static_cast<int32_t>(ScrollSnapAlign::NONE);
430     if (snapAlignValue->IsNull() || snapAlignValue->IsUndefined() || !ParseJsInt32(snapAlignValue, snapAlign) ||
431         snapAlign < static_cast<int32_t>(ScrollSnapAlign::NONE) ||
432         snapAlign > static_cast<int32_t>(ScrollSnapAlign::END)) {
433         snapAlign = static_cast<int32_t>(ScrollSnapAlign::NONE);
434     }
435 
436     auto paginationValue = obj->GetProperty("snapPagination");
437     CalcDimension intervalSize;
438     std::vector<Dimension> snapPaginations;
439     if (!ParseJsDimensionVp(paginationValue, intervalSize) || intervalSize.IsNegative()) {
440         intervalSize = CalcDimension(0.0);
441     }
442     if (!ParseJsDimensionArray(paginationValue, snapPaginations) || !CheckSnapPaginations(snapPaginations)) {
443         std::vector<Dimension>().swap(snapPaginations);
444     }
445 
446     bool enableSnapToStart = true;
447     bool enableSnapToEnd = true;
448     ParseJsBool(obj->GetProperty("enableSnapToStart"), enableSnapToStart);
449     ParseJsBool(obj->GetProperty("enableSnapToEnd"), enableSnapToEnd);
450     std::pair<bool, bool> enableSnapToSide = { enableSnapToStart, enableSnapToEnd };
451     ScrollModel::GetInstance()->SetScrollSnap(
452         static_cast<ScrollSnapAlign>(snapAlign), intervalSize, snapPaginations, enableSnapToSide);
453 }
454 
SetEnablePaging(const JSCallbackInfo & args)455 void JSScroll::SetEnablePaging(const JSCallbackInfo& args)
456 {
457     if (args.Length() < 1 || !args[0]->IsBoolean()) {
458         return;
459     }
460     ScrollModel::GetInstance()->SetEnablePaging(args[0]->ToBoolean());
461 }
462 } // namespace OHOS::Ace::Framework
463